// CCCoreLib
#include <AutoSegmentationTools.h>
#include <CCConst.h>
#include <CloudSamplingTools.h>
#include <DistanceComputationTools.h>
#include <MeshSamplingTools.h>
#include <NormalDistribution.h>
#include <StatisticalTestingTools.h>
#include <WeibullDistribution.h>

// qCC_db
#include <ccHObjectCaster.h>
#include <ccNormalVectors.h>
#include <ccPlane.h>
#include <ccPointCloudInterpolator.h>
#include <ccPolyline.h>
#include <ccProgressDialog.h>
#include <ccScalarField.h>
#include <ccSensor.h>
#include <ccSubMesh.h>
#include <ccVolumeCalcTool.h>

// qCC_io
#include <AsciiFilter.h>
#include <PlyFilter.h>

// qCC
#include "ccColorLevelsDlg.h"
#include "ccCommon.h"
#include "ccComparisonDlg.h"
#include "ccConsole.h"
#include "ccCropTool.h"
#include "ccLibAlgorithms.h"
#include "ccRegistrationTools.h"
#include "ccScalarFieldArithmeticsDlg.h"

// Qt
#include "ccCommandLineCommands.h"

// Local
#include "ccEntityAction.h"

#include <QDateTime>
#include <QFileInfo>

// commands
constexpr char COMMAND_CLOUD_EXPORT_FORMAT[]              = "C_EXPORT_FMT";
constexpr char COMMAND_EXPORT_EXTENSION[]                 = "EXT";
constexpr char COMMAND_ASCII_EXPORT_PRECISION[]           = "PREC";
constexpr char COMMAND_ASCII_EXPORT_SEPARATOR[]           = "SEP";
constexpr char COMMAND_ASCII_EXPORT_ADD_COL_HEADER[]      = "ADD_HEADER";
constexpr char COMMAND_ASCII_EXPORT_ADD_PTS_COUNT[]       = "ADD_PTS_COUNT";
constexpr char COMMAND_MESH_EXPORT_FORMAT[]               = "M_EXPORT_FMT";
constexpr char COMMAND_HIERARCHY_EXPORT_FORMAT[]          = "H_EXPORT_FMT";
constexpr char COMMAND_OPEN[]                             = "O";    //+ file name
constexpr char COMMAND_OPEN_SKIP_LINES[]                  = "SKIP"; //+ number of lines to skip
constexpr char COMMAND_OPEN_NO_LABEL[]                    = "NO_LABEL";
constexpr char COMMAND_COMMAND_FILE[]                     = "COMMAND_FILE"; //+ file name
constexpr char COMMAND_SUBSAMPLE[]                        = "SS";           //+ method (RANDOM/SPATIAL/OCTREE) + parameter (resp. point count / spatial step / octree level)
constexpr char COMMAND_EXTRACT_CC[]                       = "EXTRACT_CC";
constexpr char COMMAND_CURVATURE[]                        = "CURV";    //+ curvature type (MEAN/GAUSS)
constexpr char COMMAND_DENSITY[]                          = "DENSITY"; //+ sphere radius
constexpr char COMMAND_DENSITY_TYPE[]                     = "TYPE";    //+ density type
constexpr char COMMAND_APPROX_DENSITY[]                   = "APPROX_DENSITY";
constexpr char COMMAND_SF_GRADIENT[]                      = "SF_GRAD";
constexpr char COMMAND_ROUGHNESS[]                        = "ROUGH";
constexpr char COMMAND_ROUGHNESS_UP_DIR[]                 = "UP_DIR";
constexpr char COMMAND_APPLY_TRANSFORMATION[]             = "APPLY_TRANS";
constexpr char COMMAND_APPLY_TRANS_TO_GLOBAL[]            = "APPLY_TO_GLOBAL";
constexpr char COMMAND_APPLY_TRANS_INVERSE[]              = "INVERSE";
constexpr char COMMAND_DROP_GLOBAL_SHIFT[]                = "DROP_GLOBAL_SHIFT";
constexpr char COMMAND_SF_COLOR_SCALE[]                   = "SF_COLOR_SCALE";
constexpr char COMMAND_SF_CONVERT_TO_RGB[]                = "SF_CONVERT_TO_RGB";
constexpr char COMMAND_FILTER_SF_BY_VALUE[]               = "FILTER_SF";
constexpr char COMMAND_MERGE_CLOUDS[]                     = "MERGE_CLOUDS";
constexpr char COMMAND_MERGE_MESHES[]                     = "MERGE_MESHES";
constexpr char COMMAND_SET_ACTIVE_SF[]                    = "SET_ACTIVE_SF";
constexpr char COMMAND_SET_GLOBAL_SHIFT[]                 = "SET_GLOBAL_SHIFT"; // + global shift {x,y,z}
constexpr char COMMAND_SET_GLOBAL_SHIFT_KEEP_ORIG_FIXED[] = "KEEP_ORIG_FIXED";
constexpr char COMMAND_REMOVE_ALL_SFS[]                   = "REMOVE_ALL_SFS";
constexpr char COMMAND_REMOVE_SF[]                        = "REMOVE_SF";
constexpr char COMMAND_REMOVE_SCAN_GRIDS[]                = "REMOVE_SCAN_GRIDS";
constexpr char COMMAND_REMOVE_SENSORS[]                   = "REMOVE_SENSORS";
constexpr char COMMAND_REMOVE_RGB[]                       = "REMOVE_RGB";
constexpr char COMMAND_REMOVE_NORMALS[]                   = "REMOVE_NORMALS";
constexpr char COMMAND_MATCH_BB_CENTERS[]                 = "MATCH_CENTERS";
constexpr char COMMAND_BEST_FIT_PLANE[]                   = "BEST_FIT_PLANE";
constexpr char COMMAND_BEST_FIT_PLANE_MAKE_HORIZ[]        = "MAKE_HORIZ";
constexpr char COMMAND_BEST_FIT_PLANE_KEEP_LOADED[]       = "KEEP_LOADED";
constexpr char COMMAND_ORIENT_NORMALS[]                   = "ORIENT_NORMS_MST";
constexpr char COMMAND_SOR_FILTER[]                       = "SOR";
constexpr char COMMAND_NOISE_FILTER[]                     = "NOISE";
constexpr char COMMAND_NOISE_FILTER_KNN[]                 = "KNN";
constexpr char COMMAND_NOISE_FILTER_RADIUS[]              = "RADIUS";
constexpr char COMMAND_NOISE_FILTER_REL[]                 = "REL";
constexpr char COMMAND_NOISE_FILTER_ABS[]                 = "ABS";
constexpr char COMMAND_NOISE_FILTER_RIP[]                 = "RIP";
constexpr char COMMAND_REMOVE_DUPLICATE_POINTS[]          = "RDP";
constexpr char COMMAND_SAMPLE_MESH[]                      = "SAMPLE_MESH";
constexpr char COMMAND_COMPRESS_FWF[]                     = "COMPRESS_FWF";
constexpr char COMMAND_CROP[]                             = "CROP";
constexpr char COMMAND_CROP_OUTSIDE[]                     = "OUTSIDE";
constexpr char COMMAND_CROP_2D[]                          = "CROP2D";
constexpr char COMMAND_COLOR_BANDING[]                    = "CBANDING";
constexpr char COMMAND_COLOR_LEVELS[]                     = "CLEVELS";
constexpr char COMMAND_C2M_DIST[]                         = "C2M_DIST";
constexpr char COMMAND_C2M_DIST_FLIP_NORMALS[]            = "FLIP_NORMS";
constexpr char COMMAND_C2M_DIST_UNSIGNED[]                = "UNSIGNED";
constexpr char COMMAND_C2M_DIST_NON_ROBUST[]              = "NON_ROBUST";
constexpr char COMMAND_C2M_NORMAL_MATCHING[]              = "NORMAL_MATCH";
constexpr char COMMAND_C2C_DIST[]                         = "C2C_DIST";
constexpr char COMMAND_CLOSEST_POINT_SET[]                = "CLOSEST_POINT_SET";
constexpr char COMMAND_C2C_SPLIT_XYZ[]                    = "SPLIT_XYZ";
constexpr char COMMAND_C2C_SPLIT_XY_Z[]                   = "SPLIT_XY_Z";
constexpr char COMMAND_C2C_LOCAL_MODEL[]                  = "MODEL";
constexpr char COMMAND_C2X_MAX_DISTANCE[]                 = "MAX_DIST";
constexpr char COMMAND_C2X_OCTREE_LEVEL[]                 = "OCTREE_LEVEL";
constexpr char COMMAND_STAT_TEST[]                        = "STAT_TEST";
constexpr char COMMAND_DELAUNAY[]                         = "DELAUNAY";
constexpr char COMMAND_DELAUNAY_AA[]                      = "AA";
constexpr char COMMAND_DELAUNAY_BF[]                      = "BEST_FIT";
constexpr char COMMAND_DELAUNAY_MAX_EDGE_LENGTH[]         = "MAX_EDGE_LENGTH";
constexpr char COMMAND_SF_ARITHMETIC[]                    = "SF_ARITHMETIC";
constexpr char COMMAND_SF_ARITHMETIC_IN_PLACE[]           = "IN_PLACE";
constexpr char COMMAND_SF_OP[]                            = "SF_OP";
constexpr char COMMAND_SF_OP_NOT_IN_PLACE[]               = "NOT_IN_PLACE";
constexpr char COMMAND_SF_OP_SF[]                         = "SF_OP_SF";
constexpr char COMMAND_SF_INTERP[]                        = "SF_INTERP";
constexpr char COMMAND_COLOR_INTERP[]                     = "COLOR_INTERP";
constexpr char COMMAND_SF_INTERP_DEST_IS_FIRST[]          = "DEST_IS_FIRST";
constexpr char COMMAND_SF_ADD_CONST[]                     = "SF_ADD_CONST";
constexpr char COMMAND_SF_ADD_ID[]                        = "SF_ADD_ID";
constexpr char COMMAND_SF_ADD_ID_AS_INT[]                 = "AS_INT";
constexpr char COMMAND_RENAME_ENTITIES[]                  = "RENAME_ENTITIES"; //+ base name
constexpr char COMMAND_RENAME_SF[]                        = "RENAME_SF";
constexpr char COMMAND_COORD_TO_SF[]                      = "COORD_TO_SF";
constexpr char COMMAND_SF_TO_COORD[]                      = "SF_TO_COORD";
constexpr char COMMAND_EXTRACT_VERTICES[]                 = "EXTRACT_VERTICES";
constexpr char COMMAND_ICP[]                              = "ICP";
constexpr char COMMAND_ICP_REFERENCE_IS_FIRST[]           = "REFERENCE_IS_FIRST";
constexpr char COMMAND_ICP_MIN_ERROR_DIIF[]               = "MIN_ERROR_DIFF";
constexpr char COMMAND_ICP_ITERATION_COUNT[]              = "ITER";
constexpr char COMMAND_ICP_OVERLAP[]                      = "OVERLAP";
constexpr char COMMAND_ICP_ADJUST_SCALE[]                 = "ADJUST_SCALE";
constexpr char COMMAND_ICP_RANDOM_SAMPLING_LIMIT[]        = "RANDOM_SAMPLING_LIMIT";
constexpr char COMMAND_ICP_ENABLE_FARTHEST_REMOVAL[]      = "FARTHEST_REMOVAL";
constexpr char COMMAND_ICP_USE_MODEL_SF_AS_WEIGHT[]       = "MODEL_SF_AS_WEIGHTS";
constexpr char COMMAND_ICP_USE_DATA_SF_AS_WEIGHT[]        = "DATA_SF_AS_WEIGHTS";
constexpr char COMMAND_ICP_ROT[]                          = "ROT";
constexpr char COMMAND_ICP_SKIP_TX[]                      = "SKIP_TX";
constexpr char COMMAND_ICP_SKIP_TY[]                      = "SKIP_TY";
constexpr char COMMAND_ICP_SKIP_TZ[]                      = "SKIP_TZ";
constexpr char COMMAND_ICP_C2M_DIST[]                     = "USE_C2M_DIST";
constexpr char COMMAND_PLY_EXPORT_FORMAT[]                = "PLY_EXPORT_FMT";
constexpr char COMMAND_COMPUTE_GRIDDED_NORMALS[]          = "COMPUTE_NORMALS";
constexpr char COMMAND_INVERT_NORMALS[]                   = "INVERT_NORMALS";
constexpr char COMMAND_COMPUTE_OCTREE_NORMALS[]           = "OCTREE_NORMALS";
constexpr char COMMAND_CONVERT_NORMALS_TO_DIP[]           = "NORMALS_TO_DIP";
constexpr char COMMAND_CONVERT_NORMALS_TO_SFS[]           = "NORMALS_TO_SFS";
constexpr char COMMAND_CONVERT_NORMALS_TO_HSV[]           = "NORMALS_TO_HSV";
constexpr char COMMAND_CLEAR_NORMALS[]                    = "CLEAR_NORMALS";
constexpr char COMMAND_MESH_VOLUME[]                      = "MESH_VOLUME";
constexpr char COMMAND_VOLUME_TO_FILE[]                   = "TO_FILE";
constexpr char COMMAND_SAVE_CLOUDS[]                      = "SAVE_CLOUDS";
constexpr char COMMAND_SAVE_MESHES[]                      = "SAVE_MESHES";
constexpr char COMMAND_AUTO_SAVE[]                        = "AUTO_SAVE";
constexpr char COMMAND_LOG_FILE[]                         = "LOG_FILE";
constexpr char COMMAND_SELECT_ENTITIES[]                  = "SELECT_ENTITIES";
constexpr char COMMAND_CLEAR[]                            = "CLEAR";
constexpr char COMMAND_CLEAR_CLOUDS[]                     = "CLEAR_CLOUDS";
constexpr char COMMAND_POP_CLOUDS[]                       = "POP_CLOUDS";
constexpr char COMMAND_CLEAR_MESHES[]                     = "CLEAR_MESHES";
constexpr char COMMAND_POP_MESHES[]                       = "POP_MESHES";
constexpr char COMMAND_NO_TIMESTAMP[]                     = "NO_TIMESTAMP";
constexpr char COMMAND_MOMENT[]                           = "MOMENT";
constexpr char COMMAND_FEATURE[]                          = "FEATURE";
constexpr char COMMAND_RGB_CONVERT_TO_SF[]                = "RGB_CONVERT_TO_SF";
constexpr char COMMAND_FLIP_TRIANGLES[]                   = "FLIP_TRI";
constexpr char COMMAND_DEBUG[]                            = "DEBUG";
constexpr char COMMAND_VERBOSITY[]                        = "VERBOSITY";
constexpr char COMMAND_COMPUTE_DISTANCES_FROM_SENSOR[]    = "DISTANCES_FROM_SENSOR";
constexpr char COMMAND_COMPUTE_SCATTERING_ANGLES[]        = "SCATTERING_ANGLES";
constexpr char COMMAND_FILTER[]                           = "FILTER";

// options / modifiers
constexpr char COMMAND_MAX_THREAD_COUNT[]     = "MAX_TCOUNT";
constexpr char OPTION_ALL_AT_ONCE[]           = "ALL_AT_ONCE";
constexpr char OPTION_ON[]                    = "ON";
constexpr char OPTION_OFF[]                   = "OFF";
constexpr char OPTION_FILE_NAMES[]            = "FILE";
constexpr char OPTION_ORIENT[]                = "ORIENT";
constexpr char OPTION_MODEL[]                 = "MODEL";
constexpr char OPTION_FIRST[]                 = "FIRST";
constexpr char OPTION_LAST[]                  = "LAST";
constexpr char OPTION_ALL[]                   = "ALL";
constexpr char OPTION_REGEX[]                 = "REGEX";
constexpr char OPTION_NOT[]                   = "NOT";
constexpr char OPTION_CLOUD[]                 = "CLOUD";
constexpr char OPTION_MESH[]                  = "MESH";
constexpr char OPTION_PERCENT[]               = "PERCENT";
constexpr char OPTION_NUMBER_OF_POINTS[]      = "NUMBER_OF_POINTS";
constexpr char OPTION_FORCE[]                 = "FORCE";
constexpr char OPTION_USE_ACTIVE_SF[]         = "USE_ACTIVE_SF";
constexpr char OPTION_SF[]                    = "SF";
constexpr char OPTION_RGB[]                   = "RGB";
constexpr char OPTION_GAUSSIAN[]              = "GAUSSIAN";
constexpr char OPTION_BILATERAL[]             = "BILATERAL";
constexpr char OPTION_MEAN[]                  = "MEAN";
constexpr char OPTION_MEDIAN[]                = "MEDIAN";
constexpr char OPTION_SIGMA[]                 = "SIGMA";
constexpr char OPTION_SIGMA_SF[]              = "SIGMA_SF";
constexpr char OPTION_BURNT_COLOR_THRESHOLD[] = "BURNT_COLOR_THRESHOLD";
constexpr char OPTION_BLEND_GRAYSCALE[]       = "BLEND_GRAYSCALE";
constexpr char OPTION_SQUARED[]               = "SQUARED";
constexpr char OPTION_DEGREES[]               = "DEGREES";
constexpr char OPTION_WITH_GRIDS[]            = "WITH_GRIDS";
constexpr char OPTION_WITH_SENSOR[]           = "WITH_SENSOR";

static bool GetSFIndexOrName(ccCommandLineInterface& cmd, int& sfIndex, QString& sfName, bool allowMinusOne = false)
{
	sfName = cmd.arguments().takeFirst();
	if (sfName.toUpper() == OPTION_LAST)
	{
		sfIndex = -2;
		cmd.print(QObject::tr("SF index: LAST"));
	}
	else
	{
		bool validInt = false;
		sfIndex       = sfName.toInt(&validInt);
		if (validInt)
		{
			sfName.clear(); // user has provided an index, not a name

			if (sfIndex < 0)
			{
				if (allowMinusOne && sfIndex == -1)
				{
					// -1 means 'no active SF'
					cmd.print(QObject::tr("SF index: none"));
				}
				else
				{
					// invalid index
					cmd.warning(QObject::tr("Invalid SF index: %1").arg(sfIndex));
					return false;
				}
			}
			else
			{
				cmd.print(QObject::tr("SF index: %1").arg(sfIndex));
			}
		}
		else
		{
			cmd.print(QObject::tr("SF name: '%1'").arg(sfName));
			sfIndex = -1;
		}
	}

	return true;
}

int GetScalarFieldIndex(ccPointCloud* cloud, int sfIndex, const QString& sfName, bool minusOneMeansCurrent = false)
{
	if (!cloud)
	{
		assert(false);
		return -1;
	}
	else if (!cloud->hasScalarFields())
	{
		return -1;
	}
	else if (sfIndex == -2)
	{
		return static_cast<int>(cloud->getNumberOfScalarFields()) - 1;
	}
	else if (sfIndex == -1)
	{
		if (!sfName.isEmpty()) // the user has provided a SF name instead of an index
		{
			// check if this cloud has a scalar field with the input name
			sfIndex = cloud->getScalarFieldIndexByName(sfName.toStdString());
			if (sfIndex < 0)
			{
				ccLog::Warning(QObject::tr("Cloud %1 has no SF named '%2'").arg(cloud->getName()).arg(sfName));
				return -1;
			}
		}
		else if (minusOneMeansCurrent)
		{
			return cloud->getCurrentInScalarFieldIndex();
		}
		else
		{
			ccLog::Warning(QObject::tr("Input scalar field index is invalid: %1").arg(sfIndex));
			return -1;
		}
	}
	else if (sfIndex >= static_cast<int>(cloud->getNumberOfScalarFields()))
	{
		ccLog::Warning(QObject::tr("Cloud %1 has less scalar fields than the SF index (%2/%3)").arg(cloud->getName()).arg(sfIndex).arg(cloud->getNumberOfScalarFields()));
		return -1;
	}

	return sfIndex;
}

CCCoreLib::ScalarField* GetScalarField(ccPointCloud* cloud, int sfIndex, const QString& sfName, bool minusOneMeansCurrent = false)
{
	sfIndex = GetScalarFieldIndex(cloud, sfIndex, sfName, minusOneMeansCurrent);
	if (sfIndex < 0)
	{
		return nullptr;
	}
	else
	{
		return cloud->getScalarField(sfIndex);
	}
}

CommandChangeOutputFormat::CommandChangeOutputFormat(const QString& name, const QString& keyword)
    : ccCommandLineInterface::Command(name, keyword)
{
}

QString CommandChangeOutputFormat::getFileFormatFilter(ccCommandLineInterface& cmd, QString& defaultExt)
{
	QString fileFilter;
	defaultExt.clear();

	if (!cmd.arguments().isEmpty())
	{
		// test if the specified format corresponds to a known file type
		QString extension      = cmd.arguments().front();
		QString upperExtension = extension.toUpper();
		cmd.arguments().pop_front();

		const FileIOFilter::FilterContainer& filters = FileIOFilter::GetFilters();

		for (const auto& filter : filters)
		{
			if (upperExtension == filter->getDefaultExtension().toUpper())
			{
				// found a matching filter
				fileFilter = filter->getFileFilters(false).first();
				defaultExt = filter->getDefaultExtension();
				break;
			}
		}

		// haven't found anything?
		if (fileFilter.isEmpty())
		{
			ccLog::Warning(QString("Extension '%1' was not identified as a primary extension. Let's look for secondary ones...").arg(extension));
			// let's look for secondary formats
			for (const auto& filter : filters)
			{
				QStringList outputFilters = filter->getFileFilters(false);
				for (const QString& outputFilter : outputFilters)
				{
					int index = outputFilter.toUpper().indexOf(upperExtension);
					if (index >= 2 && outputFilter.mid(index - 2, 2) == "*.")
					{
						ccLog::Print(QString("Extension '%1' found in the output formats supported by the '%2' filter").arg(extension).arg(filter->getDefaultExtension().toUpper()));
						fileFilter = outputFilter;
						defaultExt = extension;
						break;
					}
				}

				if (!fileFilter.isEmpty())
				{
					break;
				}
			}
		}

		// still haven't found anything?
		if (fileFilter.isEmpty())
		{
			cmd.error(QObject::tr("Unhandled format specifier (%1)").arg(extension));
		}
	}
	else
	{
		cmd.error(QObject::tr("Missing file format specifier!"));
	}

	return fileFilter;
}

CommandChangeCloudOutputFormat::CommandChangeCloudOutputFormat()
    : CommandChangeOutputFormat(QObject::tr("Change cloud output format"), COMMAND_CLOUD_EXPORT_FORMAT)
{
}

bool CommandChangeCloudOutputFormat::process(ccCommandLineInterface& cmd)
{
	QString defaultExt;
	QString fileFilter = getFileFormatFilter(cmd, defaultExt);
	if (fileFilter.isEmpty())
	{
		return false;
	}

	cmd.setCloudExportFormat(fileFilter, defaultExt);
	cmd.print(QObject::tr("Output export format (clouds) set to: %1").arg(defaultExt.toUpper()));

	// default options for ASCII output
	if (fileFilter == AsciiFilter::GetFileFilter())
	{
		AsciiFilter::SetOutputCoordsPrecision(cmd.numericalPrecision());
		AsciiFilter::SetOutputSFPrecision(cmd.numericalPrecision());
		AsciiFilter::SetOutputSeparatorIndex(0); // space
		AsciiFilter::SaveSFBeforeColor(false);   // default order: point, color, SF, normal
		AsciiFilter::SaveColumnsNamesHeader(false);
		AsciiFilter::SavePointCountHeader(false);
	}

	// look for additional parameters
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_EXPORT_EXTENSION))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: extension after '%1'").arg(COMMAND_EXPORT_EXTENSION));
			}

			cmd.setCloudExportFormat(cmd.cloudExportFormat(), cmd.arguments().takeFirst());
			cmd.print(QObject::tr("New output extension for clouds: %1").arg(cmd.cloudExportExt()));
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ASCII_EXPORT_PRECISION))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: precision value after '%1'").arg(COMMAND_ASCII_EXPORT_PRECISION));
			}
			bool ok;
			int  precision = cmd.arguments().takeFirst().toInt(&ok);
			if (!ok || precision < 0)
			{
				return cmd.error(QObject::tr("Invalid value for precision! (%1)").arg(COMMAND_ASCII_EXPORT_PRECISION));
			}

			if (fileFilter != AsciiFilter::GetFileFilter())
			{
				cmd.warning(QObject::tr("Argument '%1' is only applicable to ASCII format!").arg(argument));
			}

			AsciiFilter::SetOutputCoordsPrecision(precision);
			AsciiFilter::SetOutputSFPrecision(precision);
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ASCII_EXPORT_SEPARATOR))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: separator character after '%1'").arg(COMMAND_ASCII_EXPORT_SEPARATOR));
			}

			if (fileFilter != AsciiFilter::GetFileFilter())
			{
				cmd.warning(QObject::tr("Argument '%1' is only applicable to ASCII format!").arg(argument));
			}

			QString separatorStr = cmd.arguments().takeFirst().toUpper();
			// printf("%s\n",qPrintable(separatorStr));
			int index = -1;
			if (separatorStr == "SPACE")
			{
				index = 0;
			}
			else if (separatorStr == "SEMICOLON")
			{
				index = 1;
			}
			else if (separatorStr == "COMMA")
			{
				index = 2;
			}
			else if (separatorStr == "TAB")
			{
				index = 3;
			}
			else
			{
				return cmd.error(QObject::tr("Invalid separator! ('%1')").arg(separatorStr));
			}

			AsciiFilter::SetOutputSeparatorIndex(index);
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ASCII_EXPORT_ADD_COL_HEADER))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (fileFilter != AsciiFilter::GetFileFilter())
			{
				cmd.warning(QObject::tr("Argument '%1' is only applicable to ASCII format!").arg(argument));
			}

			AsciiFilter::SaveColumnsNamesHeader(true);
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ASCII_EXPORT_ADD_PTS_COUNT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (fileFilter != AsciiFilter::GetFileFilter())
			{
				cmd.warning(QObject::tr("Argument '%1' is only applicable to ASCII format!").arg(argument));
			}

			AsciiFilter::SavePointCountHeader(true);
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	return true;
}

CommandChangeMeshOutputFormat::CommandChangeMeshOutputFormat()
    : CommandChangeOutputFormat(QObject::tr("Change mesh output format"), COMMAND_MESH_EXPORT_FORMAT)
{
}

bool CommandChangeMeshOutputFormat::process(ccCommandLineInterface& cmd)
{
	QString defaultExt;
	QString fileFilter = getFileFormatFilter(cmd, defaultExt);
	if (fileFilter.isEmpty())
	{
		return false;
	}

	cmd.setMeshExportFormat(fileFilter, defaultExt);
	cmd.print(QObject::tr("Output export format (meshes) set to: %1").arg(defaultExt.toUpper()));

	// look for additional parameters
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();

		if (ccCommandLineInterface::IsCommand(argument, COMMAND_EXPORT_EXTENSION))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: extension after '%1'").arg(COMMAND_EXPORT_EXTENSION));
			}

			cmd.setMeshExportFormat(cmd.meshExportFormat(), cmd.arguments().takeFirst());
			cmd.print(QObject::tr("New output extension for meshes: %1").arg(cmd.meshExportExt()));
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	return true;
}

CommandChangeHierarchyOutputFormat::CommandChangeHierarchyOutputFormat()
    : CommandChangeOutputFormat(QObject::tr("Change hierarchy output format"), COMMAND_HIERARCHY_EXPORT_FORMAT)
{
}

bool CommandChangeHierarchyOutputFormat::process(ccCommandLineInterface& cmd)
{
	QString defaultExt;
	QString fileFilter = getFileFormatFilter(cmd, defaultExt);
	if (fileFilter.isEmpty())
	{
		return false;
	}

	cmd.setHierarchyExportFormat(fileFilter, defaultExt);
	cmd.print(QObject::tr("Output export format (hierarchy) set to: %1").arg(defaultExt.toUpper()));

	// look for additional parameters
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();

		if (ccCommandLineInterface::IsCommand(argument, COMMAND_EXPORT_EXTENSION))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: extension after '%1'").arg(COMMAND_EXPORT_EXTENSION));
			}

			cmd.setHierarchyExportFormat(cmd.hierarchyExportFormat(), cmd.arguments().takeFirst());
			cmd.print(QObject::tr("New output extension for hierarchies: %1").arg(cmd.hierarchyExportExt()));
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	return true;
}

CommandLoad::CommandLoad()
    : ccCommandLineInterface::Command(QObject::tr("Load"), COMMAND_OPEN)
{
}

bool CommandLoad::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: filename after \"-%1\"").arg(COMMAND_OPEN));
	}

	// optional parameters
	int                                        skipLines = 0;
	ccCommandLineInterface::GlobalShiftOptions globalShiftOptions;
	bool                                       doNotCreateLabels = false;

	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_OPEN_NO_LABEL))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			cmd.print(QObject::tr("Will not load labels"));

			doNotCreateLabels = true;
		}
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_OPEN_SKIP_LINES))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: number of lines after '%1'").arg(COMMAND_OPEN_SKIP_LINES));
			}

			bool ok;
			skipLines = cmd.arguments().takeFirst().toInt(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid parameter: number of lines after '%1'").arg(COMMAND_OPEN_SKIP_LINES));
			}

			cmd.print(QObject::tr("Will skip %1 lines").arg(skipLines));
		}
		else if (cmd.nextCommandIsGlobalShift())
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (!cmd.processGlobalShiftCommand(globalShiftOptions))
			{
				// error message already issued
				return false;
			}
		}
		else
		{
			break;
		}
	}

	if (skipLines >= 0)
	{
		AsciiFilter::SetDefaultSkippedLineCount(skipLines);
	}
	AsciiFilter::SetNoLabelCreated(doNotCreateLabels);

	// open specified file
	QString filename(cmd.arguments().takeFirst());
	if (!cmd.importFile(filename, globalShiftOptions))
	{
		return false;
	}

	return true;
}

CommandLoadCommandFile::CommandLoadCommandFile()
    : ccCommandLineInterface::Command(QObject::tr("Load commands from file"), COMMAND_COMMAND_FILE)
{
}

bool CommandLoadCommandFile::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: filename after \"-%1\"").arg(COMMAND_COMMAND_FILE));
	}
	QString commandFilePath = cmd.arguments().takeFirst();

	// check if file exists
	if (!QFileInfo::exists(commandFilePath))
	{
		return cmd.error(QObject::tr("Command file not exists \"-%1\"").arg(commandFilePath));
	}

	QFile commandFile(commandFilePath);
	if (commandFile.open(QIODevice::ReadOnly))
	{
		int         insertingIndex = 0;
		QTextStream in(&commandFile);
		while (!in.atEnd())
		{
			QString line = in.readLine().trimmed();

			// early abort whole line comments
			if (line.startsWith("#") || line.startsWith("//"))
			{
				cmd.print(QObject::tr("\t[COMMENT] %1").arg(line));
				continue;
			}

			QStringList argumentsInLine = line.split(" ");
			QStringList processedArgs;

			// 'massage' the arguments to handle single/double quotes
			// TODO handle escaped quotes/spaces
			{
				bool    insideSingleQuoteSection = false;
				bool    insideDoubleQuoteSection = false;
				QString buffer;

				static const QChar SingleQuote{'\''};
				static const QChar DoubleQuote{'"'};
				for (int currentArgIndex = 0; currentArgIndex < argumentsInLine.size(); ++currentArgIndex)
				{
					QString arg = argumentsInLine[currentArgIndex];
					// handle singleQuotes
					{
						// argument starts with a single quote and not inside double quotes
						if (!insideSingleQuoteSection && !insideDoubleQuoteSection && arg.startsWith(SingleQuote))
						{
							if (arg.endsWith(SingleQuote))
							{
								arg.remove(SingleQuote);
								// nothing to do, non*truncated argument
							}
							else
							{
								// we'll collect the next pieces to get the full argument
								insideSingleQuoteSection = true;
								buffer                   = arg.mid(1); // remove the single quote
							}
						}
						else if (insideSingleQuoteSection)
						{
							buffer += QChar(' ') + arg; // append the current argument to the previous one(s)
							if (arg.endsWith(SingleQuote))
							{
								insideSingleQuoteSection = false;
								arg                      = buffer.left(buffer.length() - 1); // remove the single quote
								arg.remove(SingleQuote);
							}
						}
					}
					// handle doubleQuotes
					{
						// argument starts with a double quote and not inside single quotes
						if (!insideSingleQuoteSection && !insideDoubleQuoteSection && arg.startsWith(DoubleQuote))
						{
							if (arg.endsWith(DoubleQuote))
							{
								arg.remove(DoubleQuote);
								// nothing to do, non*truncated argument
							}
							else
							{
								// we'll collect the next pieces to get the full argument
								insideDoubleQuoteSection = true;
								buffer                   = arg.mid(1); // remove the double quote
							}
						}
						else if (insideDoubleQuoteSection)
						{
							buffer += QChar(' ') + arg; // append the current argument to the previous one(s)
							if (arg.endsWith(DoubleQuote))
							{
								insideDoubleQuoteSection = false;
								arg                      = buffer.left(buffer.length() - 1); // remove the double quote
								arg.remove(DoubleQuote);
							}
						}
					}
					if (!insideSingleQuoteSection && !insideDoubleQuoteSection)
					{
						processedArgs.append(arg);
					}
				}
				if (insideSingleQuoteSection || insideDoubleQuoteSection)
				{
					// the single/double quote section was not closed...
					cmd.warning("Probably malformed command (missing closing quote)");
					// ...still, we'll try to proceed
					processedArgs.append(buffer);
				}
			}

			// inject back all the arguments to the cmd.arguments()
			while (!processedArgs.isEmpty())
			{
				QString processedArg = processedArgs.takeFirst();
				if (!processedArg.isEmpty())
				{
					if (processedArg.startsWith("//") || processedArg.startsWith("#"))
					{
						// abort and keep the rest in processedArgs
						break;
					}

					if (!(processedArg.startsWith("/*") && processedArg.endsWith("*/")))
					{
						// standard argument
						cmd.arguments().insert(insertingIndex, processedArg);
						insertingIndex++;
						cmd.print(QObject::tr("\t[%1] %2").arg(insertingIndex - 1).arg(processedArg));
					}
					else
					{
						cmd.print(QObject::tr("\t[COMMENT] %1").arg(processedArg));
					}
				}
			}

			// if any arguments left, then it is a comment
			if (!processedArgs.isEmpty())
			{
				cmd.print(QObject::tr("\t[COMMENT] %1").arg(processedArgs.join(" ")));
			}
		}
		commandFile.close();
	}

	return true;
}

CommandClearNormals::CommandClearNormals()
    : ccCommandLineInterface::Command(QObject::tr("Clears normals"), COMMAND_CLEAR_NORMALS)
{
}

bool CommandClearNormals::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No entity loaded (be sure to open at least one file with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_CLEAR_NORMALS));
	}

	for (const CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;
		if (cloud)
		{
			cloud->unallocateNorms();
		}
	}

	for (const CLMeshDesc& thisMeshDesc : cmd.meshes())
	{
		ccMesh* mesh = ccHObjectCaster::ToMesh(thisMeshDesc.mesh);
		if (!mesh)
		{
			assert(false);
			continue;
		}

		mesh->clearTriNormals();

		if (mesh->getParent()
		    && (mesh->getParent()->isA(CC_TYPES::MESH) /*|| mesh->getParent()->isKindOf(CC_TYPES::PRIMITIVE)*/) // TODO
		    && ccHObjectCaster::ToMesh(mesh->getParent())->getAssociatedCloud() == mesh->getAssociatedCloud())
		{
			// Can't remove per-vertex normals on a sub mesh!
		}
		else
		{
			// mesh is alone, we can freely remove normals
			if (mesh->getAssociatedCloud() && mesh->getAssociatedCloud()->isA(CC_TYPES::POINT_CLOUD))
			{
				static_cast<ccPointCloud*>(mesh->getAssociatedCloud())->unallocateNorms();
			}
		}
	}

	return true;
}

CommandInvertNormal::CommandInvertNormal()
    : ccCommandLineInterface::Command(QObject::tr("Invert normals"), COMMAND_INVERT_NORMALS)
{
}

bool CommandInvertNormal::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No input point cloud or mesh (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_INVERT_NORMALS));
	}

	for (CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;

		if (!cloud->hasNormals())
		{
			cmd.warning(QObject::tr("Cloud %1 has no normals").arg(cloud->getName()));
			continue;
		}

		cloud->invertNormals();

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(thisCloudDesc, "_INVERTED_NORMALS");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	for (CLMeshDesc& thisMeshDesc : cmd.meshes())
	{
		ccMesh* mesh = ccHObjectCaster::ToMesh(thisMeshDesc.mesh);
		if (!mesh)
		{
			assert(false);
			continue;
		}

		if (!mesh->hasNormals())
		{
			cmd.warning(QObject::tr("Mesh %1 has no normals").arg(mesh->getName()));
			continue;
		}

		mesh->invertNormals();

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(thisMeshDesc, "_INVERTED_NORMALS");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}
CommandOctreeNormal::CommandOctreeNormal()
    : ccCommandLineInterface::Command(QObject::tr("Compute normals with octree"), COMMAND_COMPUTE_OCTREE_NORMALS)
{
}

bool CommandOctreeNormal::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud to compute normals (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_COMPUTE_OCTREE_NORMALS));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: radius after \"-%1\"").arg(COMMAND_COMPUTE_OCTREE_NORMALS));
	}

	float   radius    = std::numeric_limits<float>::quiet_NaN(); // if this stays
	QString radiusArg = cmd.arguments().takeFirst();
	if (radiusArg.toUpper() != "AUTO")
	{
		bool ok = false;
		radius  = radiusArg.toFloat(&ok);
		if (!ok)
		{
			return cmd.error(QObject::tr("Invalid radius"));
		}
	}
	cmd.print(QObject::tr("\tRadius: %1").arg(radiusArg));

	CCCoreLib::LOCAL_MODEL_TYPES model       = CCCoreLib::QUADRIC;
	ccNormalVectors::Orientation orientation = ccNormalVectors::Orientation::UNDEFINED;

	bool  useGridStructure         = false;
	bool  orientNormalsWithGrids   = false;
	bool  orientNormalsWithSensors = false;
	float angle                    = std::numeric_limits<float>::quiet_NaN(); // if this stays
	while (!cmd.arguments().isEmpty())
	{
		QString argument = cmd.arguments().front().toUpper();
		if (ccCommandLineInterface::IsCommand(argument, OPTION_ORIENT))
		{
			cmd.arguments().takeFirst();
			if (!cmd.arguments().isEmpty())
			{
				QString orient_argument = cmd.arguments().takeFirst().toUpper();
				if (orient_argument == "PLUS_ZERO" || orient_argument == "PLUS_ORIGIN")
				{
					orientation = ccNormalVectors::Orientation::PLUS_ORIGIN;
				}
				else if (orient_argument == "MINUS_ZERO" || orient_argument == "MINUS_ORIGIN")
				{
					orientation = ccNormalVectors::Orientation::MINUS_ORIGIN;
				}
				else if (orient_argument == "PLUS_BARYCENTER")
				{
					orientation = ccNormalVectors::Orientation::PLUS_BARYCENTER;
				}
				else if (orient_argument == "MINUS_BARYCENTER")
				{
					orientation = ccNormalVectors::Orientation::MINUS_BARYCENTER;
				}
				else if (orient_argument == "PLUS_X")
				{
					orientation = ccNormalVectors::Orientation::PLUS_X;
				}
				else if (orient_argument == "MINUS_X")
				{
					orientation = ccNormalVectors::Orientation::MINUS_X;
				}
				else if (orient_argument == "PLUS_Y")
				{
					orientation = ccNormalVectors::Orientation::PLUS_Y;
				}
				else if (orient_argument == "MINUS_Y")
				{
					orientation = ccNormalVectors::Orientation::MINUS_Y;
				}
				else if (orient_argument == "PLUS_Z")
				{
					orientation = ccNormalVectors::Orientation::PLUS_Z;
				}
				else if (orient_argument == "MINUS_Z")
				{
					orientation = ccNormalVectors::Orientation::MINUS_Z;
				}
				else if (orient_argument == "PREVIOUS")
				{
					orientation = ccNormalVectors::Orientation::PREVIOUS;
				}
				else if (orient_argument == "PLUS_SENSOR_ORIGIN")
				{
					orientation = ccNormalVectors::Orientation::PLUS_SENSOR_ORIGIN;
				}
				else if (orient_argument == "MINUS_SENSOR_ORIGIN")
				{
					orientation = ccNormalVectors::Orientation::MINUS_SENSOR_ORIGIN;
				}
				else if (orient_argument == OPTION_WITH_GRIDS)
				{
					orientation            = ccNormalVectors::Orientation::UNDEFINED;
					orientNormalsWithGrids = true;
				}
				else if (orient_argument == OPTION_WITH_SENSOR)
				{
					orientation              = ccNormalVectors::Orientation::UNDEFINED;
					orientNormalsWithSensors = true;
				}
				else
				{
					return cmd.error(QObject::tr("Invalid parameter: unknown orientation '%1'").arg(orient_argument));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing orientation"));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_MODEL))
		{
			cmd.arguments().takeFirst();
			if (!cmd.arguments().isEmpty())
			{
				QString model_arg = cmd.arguments().takeFirst().toUpper();
				if (model_arg == "LS")
				{
					model = CCCoreLib::LOCAL_MODEL_TYPES::LS;
				}
				else if (model_arg == "TRI")
				{
					model = CCCoreLib::LOCAL_MODEL_TYPES::TRI;
				}
				else if (model_arg == "QUADRIC")
				{
					model = CCCoreLib::LOCAL_MODEL_TYPES::QUADRIC;
				}
				else
				{
					return cmd.error(QObject::tr("Invalid parameter: unknown model '%1'").arg(model_arg));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing model"));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_WITH_GRIDS))
		{
			cmd.arguments().takeFirst();
			if (!cmd.arguments().isEmpty())
			{
				bool    ok       = false;
				QString angleArg = cmd.arguments().takeFirst();
				angle            = angleArg.toFloat(&ok);
				if (!ok)
				{
					return cmd.error(QObject::tr("Invalid angle for scan grids"));
				}
				cmd.print(QObject::tr("\tAngle for scan grids: %1").arg(angleArg));
				useGridStructure = true;
			}
			else
			{
				return cmd.error(QObject::tr("Missing min angle for scan grids"));
			}
		}
		else
		{
			break;
		}
	}

	for (const CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;

		QScopedPointer<ccProgressDialog> progressDialog(nullptr);
		if (!cmd.silentMode())
		{
			progressDialog.reset(new ccProgressDialog(true, cmd.widgetParent()));
			progressDialog->setAutoClose(false);
		}

		if (!cloud->getOctree())
		{
			if (!cloud->computeOctree(progressDialog.data()))
			{
				return cmd.error(QObject::tr("Failed to compute octree for cloud '%1'").arg(cloud->getName()));
			}
		}

		float thisCloudRadius = radius;
		if (std::isnan(thisCloudRadius))
		{
			ccOctree::BestRadiusParams params;
			{
				params.aimedPopulationPerCell = 16;
				params.aimedPopulationRange   = 4;
				params.minCellPopulation      = 6;
				params.minAboveMinRatio       = 0.97;
			}
			thisCloudRadius = ccOctree::GuessBestRadius(cloud, params, cloud->getOctree().data());
			if (thisCloudRadius == 0)
			{
				return cmd.error(QObject::tr("Failed to determine best normal radius for cloud '%1'").arg(cloud->getName()));
			}
			cmd.print(QObject::tr("\tCloud %1 radius = %2").arg(cloud->getName()).arg(thisCloudRadius));
		}

		bool success                = false;
		bool normalsAlreadyOriented = false;
		if (useGridStructure)
		{
			if (orientation == ccNormalVectors::UNDEFINED)
			{
				ccLog::Print("\tcompute + orient normals with grids");
				success = cloud->computeNormalsWithGrids(angle, progressDialog.data(), ccNormalVectors::UNDEFINED);
				if (orientNormalsWithGrids)
				{
					normalsAlreadyOriented = true;
				}
			}
			else
			{
				ccLog::Print("\tcompute normals with grids, preferred orientation: " + QString::number(orientation));
				success = cloud->computeNormalsWithGrids(angle, progressDialog.data(), orientation);
			}
			if (!success)
			{
				return cmd.error(QObject::tr("computeNormalsWithGrids failed"));
			}
		}
		else
		{
			ccLog::Print("\tcompute normals with octree, preferred orientation: " + QString::number(orientation));
			success = cloud->computeNormalsWithOctree(model, orientation, thisCloudRadius, progressDialog.data());
			if (!success)
			{
				return cmd.error(QObject::tr("computeNormalsWithOctree failed"));
			}
		}

		// ORIENT WITH_GRID
		if (cloud->gridCount() && orientNormalsWithGrids && !normalsAlreadyOriented)
		{
			// we can use the grid structure(s) to orient the normals
			if (cloud->orientNormalsWithGrids())
			{
				cmd.print(QObject::tr("\torient normals with grids"));
			}
			else
			{
				return cmd.error(QObject::tr("Orient normals with grids failed"));
			}
		}

		// ORIENT WITH_SENSOR
		else if (cloud->hasSensor() && orientNormalsWithSensors)
		{
			// RJ: TODO: the issue here is that a cloud can have multiple sensors.
			// As the association to sensor is not explicit in CC, given a cloud
			// some points can belong to one sensor and some others can belongs to others sensors.
			// so it's why here grid orientation has precedence over sensor orientation because in this
			// case association is more explicit.
			// Here we take the first valid viewpoint for now even if it's not a good one...
			for (unsigned i = 0; i < cloud->getChildrenNumber(); ++i)
			{
				ccHObject* child = cloud->getChild(i);
				if (child && child->isKindOf(CC_TYPES::SENSOR))
				{
					ccSensor* sensor = ccHObjectCaster::ToSensor(child);
					CCVector3 sensorPosition;
					if (sensor->getActiveAbsoluteCenter(sensorPosition))
					{
						if (cloud->orientNormalsTowardViewPoint(sensorPosition))
						{
							cmd.print(QObject::tr("\torient normals with sensors"));
						}
						else
						{
							return cmd.error(QObject::tr("Orient normals with sensors failed"));
						}
					}
				}
			}
		}

		cloud->setName(cloud->getName() + QObject::tr(".OctreeNormal"));
		if (cmd.autoSaveMode())
		{
			CLCloudDesc desc(cloud, thisCloudDesc.basename, thisCloudDesc.path, thisCloudDesc.indexInFile);
			QString     errorStr = cmd.exportEntity(desc, "OCTREE_NORMALS");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandConvertNormalsToDipAndDipDir::CommandConvertNormalsToDipAndDipDir()
    : ccCommandLineInterface::Command(QObject::tr("Convert normals to dip and dip. dir."), COMMAND_CONVERT_NORMALS_TO_DIP)
{
}

bool CommandConvertNormalsToDipAndDipDir::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No input point cloud (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_CONVERT_NORMALS_TO_DIP));
	}

	for (CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;

		if (!cloud->hasNormals())
		{
			cmd.warning(QObject::tr("Cloud %1 has no normals").arg(cloud->getName()));
			continue;
		}

		ccHObject::Container container;
		container.push_back(cloud);
		if (!ccEntityAction::convertNormalsTo(container, ccEntityAction::NORMAL_CONVERSION_DEST::DIP_DIR_SFS))
		{
			return cmd.error(QObject::tr("Failed to convert normals to dip and dip direction"));
		}

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(thisCloudDesc, "_DIP_AND_DIP_DIR");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandConvertNormalsToSFs::CommandConvertNormalsToSFs()
    : ccCommandLineInterface::Command(QObject::tr("Convert normals to scalar fields"), COMMAND_CONVERT_NORMALS_TO_SFS)
{
}

bool CommandConvertNormalsToSFs::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No input point cloud (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_CONVERT_NORMALS_TO_SFS));
	}

	for (CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;

		if (!cloud->hasNormals())
		{
			cmd.warning(QObject::tr("Cloud %1 has no normals").arg(cloud->getName()));
			continue;
		}

		bool exportDims[3] = {true, true, true};

		ccHObject::Container container;
		container.push_back(cloud);
		if (!ccEntityAction::exportNormalToSF(container, cmd.widgetParent(), exportDims))
		{
			return cmd.error(QObject::tr("Failed to convert normals to scalar fields"));
		}

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(thisCloudDesc, "_NORM_TO_SF");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandConvertNormalsToHSV::CommandConvertNormalsToHSV()
    : ccCommandLineInterface::Command(QObject::tr("Convert normals to HSV colors"), COMMAND_CONVERT_NORMALS_TO_HSV)
{
}

bool CommandConvertNormalsToHSV::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No input point cloud (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_CONVERT_NORMALS_TO_HSV));
	}

	for (CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		ccPointCloud* cloud = thisCloudDesc.pc;

		if (!cloud->hasNormals())
		{
			cmd.warning(QObject::tr("Cloud %1 has no normals").arg(cloud->getName()));
			continue;
		}

		ccHObject::Container container;
		container.push_back(cloud);
		if (!ccEntityAction::convertNormalsTo(container, ccEntityAction::NORMAL_CONVERSION_DEST::HSV_COLORS))
		{
			return cmd.error(QObject::tr("Failed to convert normals to HSV colors"));
		}

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(thisCloudDesc, "_HSV");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandSubsample::CommandSubsample()
    : ccCommandLineInterface::Command(QObject::tr("Subsample"), COMMAND_SUBSAMPLE)
{
}

bool CommandSubsample::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud to resample (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SUBSAMPLE));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: resampling method after \"-%1\"").arg(COMMAND_SUBSAMPLE));
	}

	QString method = cmd.arguments().takeFirst().toUpper();
	cmd.print(QObject::tr("\tMethod: ") + method);
	if (method == "RANDOM")
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: number of points or option \"%2\" after \"-%1 RANDOM \"").arg(COMMAND_SUBSAMPLE).arg(OPTION_PERCENT));
		}
		bool     isPercent = false;
		double   percent   = 0.0;
		unsigned count     = 0;

		// handle percent argument
		if (cmd.arguments().front() == OPTION_PERCENT)
		{
			// local option verified
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: number after \"-%1 RANDOM %2\"").arg(COMMAND_SUBSAMPLE).arg(OPTION_PERCENT));
			}

			bool ok;
			percent = cmd.arguments().takeFirst().toDouble(&ok);
			if (!ok || percent < 0 || percent > 100)
			{
				return cmd.error(QObject::tr("Invalid parameter: number after \"-%1 RANDOM %2\" must be decimal between 0 and 100").arg(COMMAND_SUBSAMPLE).arg(OPTION_PERCENT));
			}

			isPercent = true;
		}
		else
		{
			bool ok;
			count = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid parameter: number of points or option \"%2\" after \"-%1 RANDOM \"").arg(COMMAND_SUBSAMPLE).arg(OPTION_PERCENT));
			}
			cmd.print(QObject::tr("\tOutput points: %1").arg(count));
		}

		for (CLCloudDesc& desc : cmd.clouds())
		{
			cmd.print(QObject::tr("\tProcessing cloud %1").arg(!desc.pc->getName().isEmpty() ? desc.pc->getName() : "no name"));

			if (isPercent)
			{
				size_t nrOfPoints = desc.pc->size();
				count             = static_cast<unsigned>(ceil(nrOfPoints * percent / 100));
				cmd.print(QObject::tr("\tOutput points: %1 * %2% = %3").arg(nrOfPoints).arg(percent).arg(count));
			}

			CCCoreLib::ReferenceCloud* refCloud = CCCoreLib::CloudSamplingTools::subsampleCloudRandomly(desc.pc, count, cmd.progressDialog());
			if (!refCloud)
			{
				return cmd.error(QObject::tr("Subsampling process failed!"));
			}
			cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));

			// save output
			ccPointCloud* result = desc.pc->partialClone(refCloud);
			delete refCloud;
			refCloud = nullptr;

			if (result)
			{
				result->setName(desc.pc->getName() + QObject::tr(".subsampled"));
				if (cmd.autoSaveMode())
				{
					CLCloudDesc newDesc(result, desc.basename, desc.path, desc.indexInFile);
					QString     errorStr = cmd.exportEntity(newDesc, "RANDOM_SUBSAMPLED");
					if (!errorStr.isEmpty())
					{
						delete result;
						return cmd.error(errorStr);
					}
				}
				// replace current cloud by this one
				delete desc.pc;
				desc.pc = result;
				desc.basename += "_RANDOM_SUBSAMPLED";
			}
			else
			{
				return cmd.error(QObject::tr("Not enough memory!"));
			}
		}
	}
	else if (method == "SPATIAL")
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: spatial step after \"-%1 SPATIAL\"").arg(COMMAND_SUBSAMPLE));
		}

		bool   ok;
		double step = cmd.arguments().takeFirst().toDouble(&ok);
		if (!ok || step <= 0)
		{
			return cmd.error(QObject::tr("Invalid step value for spatial subsampling!"));
		}
		cmd.print(QObject::tr("\tSpatial step: %1").arg(step));

		double sfMinSpacing = 0;
		double sfMaxSpacing = 0;
		bool   useActiveSF  = false;
		if (!cmd.arguments().empty())
		{
			if (cmd.arguments().front().toUpper() == OPTION_USE_ACTIVE_SF)
			{
				// enable USE_ACTIVE_SF
				useActiveSF = true;
				cmd.arguments().pop_front();
				if (cmd.arguments().size() >= 2)
				{
					bool validMin = false;
					sfMinSpacing  = cmd.arguments().takeFirst().toDouble(&validMin);
					bool validMax = false;
					sfMaxSpacing  = cmd.arguments().takeFirst().toDouble(&validMax);
					if (!validMin || !validMax || sfMinSpacing < 0 || sfMaxSpacing < 0)
					{
						return cmd.error(QObject::tr("Invalid parameters: Two positive decimal number required after '%1'").arg(OPTION_USE_ACTIVE_SF));
					}
				}
				else
				{
					return cmd.error(QObject::tr("Missing parameters: Two positive decimal number required after '%1'").arg(OPTION_USE_ACTIVE_SF));
				}
			}
		}

		for (CLCloudDesc& desc : cmd.clouds())
		{
			cmd.print(QObject::tr("\tProcessing cloud %1").arg(!desc.pc->getName().isEmpty() ? desc.pc->getName() : "no name"));

			CCCoreLib::CloudSamplingTools::SFModulationParams modParams(false);

			// handle Use Active SF on each cloud
			if (useActiveSF)
			{
				// look for the min and max sf values
				ccScalarField* sf = desc.pc->getCurrentDisplayedScalarField();
				if (!sf)
				{
					// warn the user, not use active SF and keep going
					cmd.warning(QObject::tr("\tCan't use 'Use active SF': no active scalar field. Set one with '-%1'").arg(COMMAND_SET_ACTIVE_SF));
				}
				else
				{
					// found active scalar field
					ScalarType sfMin = CCCoreLib::NAN_VALUE;
					ScalarType sfMax = CCCoreLib::NAN_VALUE;
					if (sf->countValidValues() > 0)
					{
						if (!ccScalarField::ValidValue(sfMin) || sfMin > sf->getMin())
							sfMin = sf->getMin();
						if (!ccScalarField::ValidValue(sfMax) || sfMax < sf->getMax())
							sfMax = sf->getMax();
						if (!ccScalarField::ValidValue(sfMin) || !ccScalarField::ValidValue(sfMax))
						{
							// warn the user, don't use 'Use Active SF' and keep going
							cmd.warning(QObject::tr("\tCan't use 'Use active SF': scalar field '%1' has invalid min/max values.").arg(QString::fromStdString(sf->getName())));
						}
						else
						{
							// everything validated use acitve SF for modulation
							// implementation of modParams.a/b values come from MainWindow::doActionSubsample()
							modParams.enabled = true;

							double deltaSF = static_cast<double>(sfMax) - static_cast<double>(sfMin);

							if (CCCoreLib::GreaterThanEpsilon(deltaSF))
							{
								modParams.a = (sfMaxSpacing - sfMinSpacing) / deltaSF;
								modParams.b = sfMinSpacing - modParams.a * sfMin;
							}
							else
							{
								modParams.a = 0.0;
								modParams.b = sfMin;
							}
							cmd.print(QObject::tr("\tUse active SF: enabled\n\t\tSpacing at SF min (%1): %2\n\t\tSpacing at SF max (%3): %4")
							              .arg(sfMin)
							              .arg(sfMinSpacing)
							              .arg(sfMax)
							              .arg(sfMaxSpacing));
						}
					}
					else
					{
						// warn the user, not use Use Active SF and keep going
						cmd.warning(QObject::tr("\tCan't use 'Use active SF': scalar field '%2' does not have any valid value.").arg(COMMAND_SET_ACTIVE_SF).arg(QString::fromStdString(sf->getName())));
					}
				}
			}

			if (useActiveSF && !modParams.enabled)
			{
				cmd.print(QObject::tr("\t'Use active SF' disabled. Falling back to constant spacing."));
			}

			CCCoreLib::ReferenceCloud* refCloud = CCCoreLib::CloudSamplingTools::resampleCloudSpatially(desc.pc, static_cast<PointCoordinateType>(step), modParams, nullptr, cmd.progressDialog());
			if (!refCloud)
			{
				return cmd.error("Subsampling process failed!");
			}
			cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));

			// save output
			ccPointCloud* result = desc.pc->partialClone(refCloud);
			delete refCloud;
			refCloud = nullptr;

			if (result)
			{
				result->setName(desc.pc->getName() + QObject::tr(".subsampled"));
				if (cmd.autoSaveMode())
				{
					CLCloudDesc newDesc(result, desc.basename, desc.path, desc.indexInFile);
					QString     errorStr = cmd.exportEntity(newDesc, "SPATIAL_SUBSAMPLED");
					if (!errorStr.isEmpty())
					{
						delete result;
						return cmd.error(errorStr);
					}
				}
				// replace current cloud by this one
				delete desc.pc;
				desc.pc = result;
				desc.basename += "_SPATIAL_SUBSAMPLED";
			}
			else
			{
				return cmd.error(QObject::tr("Not enough memory!"));
			}
		}
	}
	else if (method == "OCTREE")
	{
		int       octreeLevel         = 1;
		double    cellSize            = 0.0;
		bool      byCellSize          = false;
		bool      byMaxNumberOfPoints = false;
		unsigned  maxNumberOfPoints   = 0;
		bool      isPercent           = false;
		double    percent             = 0.0;
		const int maxOctreeLevel      = CCCoreLib::DgmOctree::MAX_OCTREE_LEVEL;

		if (!cmd.arguments().empty())
		{
			// params for automatic OCTREE level calculation based on cell size
			if (cmd.arguments().front() == "CELL_SIZE")
			{
				cmd.arguments().pop_front();

				if (cmd.arguments().empty())
				{
					return cmd.error(QObject::tr("Missing parameter: octree cell size after \"-%1 OCTREE CELL_SIZE \"").arg(COMMAND_SUBSAMPLE));
				}

				bool ok  = false;
				cellSize = cmd.arguments().takeFirst().toDouble(&ok);
				if (!ok)
				{
					return cmd.error(QObject::tr("Invalid parameter: octree cell size after \"-%1 OCTREE CELL_SIZE \"").arg(COMMAND_SUBSAMPLE));
				}
				byCellSize = true;
				cmd.print(QObject::tr("\tOctree cell size: %1").arg(cellSize));
			}

			// params for automatic OCTREE level calculation based on number of points
			else if (cmd.arguments().front() == OPTION_NUMBER_OF_POINTS)
			{
				// local option verified
				byMaxNumberOfPoints = true;
				cmd.arguments().pop_front();

				if (cmd.arguments().empty())
				{
					return cmd.error(QObject::tr("Missing parameter: number of points or option \"%3\" after \"-%1 OCTREE %2 \"").arg(COMMAND_SUBSAMPLE).arg(OPTION_NUMBER_OF_POINTS).arg(OPTION_PERCENT));
				}

				// handle percent argument
				if (cmd.arguments().front() == OPTION_PERCENT)
				{
					// local option verified
					cmd.arguments().pop_front();
					if (cmd.arguments().empty())
					{
						return cmd.error(QObject::tr("Missing parameter: number after \"-%1 OCTREE %2 %3\"").arg(COMMAND_SUBSAMPLE).arg(OPTION_NUMBER_OF_POINTS).arg(OPTION_PERCENT));
					}

					bool ok = false;
					percent = cmd.arguments().takeFirst().toDouble(&ok);
					if (!ok || percent < 0 || percent > 100)
					{
						return cmd.error(QObject::tr("Invalid parameter: number after \"-%1 OCTREE %2 %3\" must be decimal between 0 and 100").arg(COMMAND_SUBSAMPLE).arg(OPTION_NUMBER_OF_POINTS).arg(OPTION_PERCENT));
					}

					isPercent = true;
				}
				else
				{
					bool ok           = false;
					maxNumberOfPoints = cmd.arguments().takeFirst().toUInt(&ok);
					if (!ok)
					{
						return cmd.error(QObject::tr("Invalid parameter: number of points or option \"%3\" after \"-%1 OCTREE %2 \"").arg(COMMAND_SUBSAMPLE).arg(OPTION_NUMBER_OF_POINTS).arg(OPTION_PERCENT));
					}
					cmd.print(QObject::tr("\tOctree target number of points: %1").arg(maxNumberOfPoints));
				}
			}
			// params for original version octree calculation based on given level
			else
			{
				if (cmd.arguments().empty())
				{
					return cmd.error(QObject::tr("Missing parameter: octree level after \"-%1 OCTREE\"").arg(COMMAND_SUBSAMPLE));
				}

				bool ok     = false;
				octreeLevel = cmd.arguments().takeFirst().toInt(&ok);
				if (!ok || octreeLevel < 1 || octreeLevel > maxOctreeLevel)
				{
					return cmd.error(QObject::tr("Invalid octree level!"));
				}
				cmd.print(QObject::tr("\tOctree level: %1").arg(octreeLevel));
			}
		}

		QScopedPointer<ccProgressDialog> progressDialog(nullptr);
		if (!cmd.silentMode())
		{
			progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
			progressDialog->setAutoClose(false);
		}

		for (CLCloudDesc& desc : cmd.clouds())
		{
			// calculate octree before subsampling, it is passed to subsampling, so it won't be recalculated there.
			CCCoreLib::DgmOctree* octree = desc.pc->getOctree().data();
			if (!octree)
			{
				octree = desc.pc->computeOctree(nullptr, false).data();
			}
			if (!octree)
			{
				return cmd.error("Octree calculation failed, not enough memory?");
			}
			CCCoreLib::ReferenceCloud* refCloud         = nullptr;
			ccPointCloud*              result           = nullptr;
			unsigned                   sizeOfInputCloud = desc.pc->size();

			cmd.print(QObject::tr("\tProcessing cloud %1").arg(!desc.pc->getName().isEmpty() ? desc.pc->getName() : "no name"));

			if (byCellSize)
			{
				// calculate OCTREE level for each cloud based on required octree cell size
				octreeLevel = static_cast<int>(ceil(log(desc.pc->getOwnBB().getMaxBoxDim() / cellSize) / log(2.0)));
			}

			if (byMaxNumberOfPoints)
			{
				if (isPercent)
				{
					maxNumberOfPoints = static_cast<unsigned>(ceil(sizeOfInputCloud * percent / 100));
					cmd.print(QObject::tr("\tOutput point target: %1 * %2% = %3").arg(sizeOfInputCloud).arg(percent).arg(maxNumberOfPoints));
				}

				// calculate OCTREE level for each cloud based on required number of points
				octreeLevel             = maxOctreeLevel;
				unsigned numberOfPoints = sizeOfInputCloud;
				// go through max->min until previous point count and current point count is different then break
				for (int currentOctreeLevel = maxOctreeLevel; currentOctreeLevel > 0; --currentOctreeLevel)
				{
					unsigned currentNumberOfPoints = octree->getCellNumber(currentOctreeLevel);
					if (currentNumberOfPoints != numberOfPoints && numberOfPoints < maxNumberOfPoints)
					{
						break;
					}
					octreeLevel    = currentOctreeLevel;
					numberOfPoints = currentNumberOfPoints;
				}
			}

			// only process further if CELL_SIZE or octree level was given, or the numberOfPoints smaller than the input cloud
			if (!byMaxNumberOfPoints || (byMaxNumberOfPoints && sizeOfInputCloud > maxNumberOfPoints))
			{
				// overwrite and print out finalized OCTREE level.
				if (byCellSize || byMaxNumberOfPoints)
				{
					// clamp octree level
					octreeLevel = std::max(std::min(octreeLevel, maxOctreeLevel), 1);
					cmd.print(QObject::tr("\tCalculated octree level: %1").arg(octreeLevel));
				}
				refCloud = CCCoreLib::CloudSamplingTools::subsampleCloudWithOctreeAtLevel(desc.pc,
				                                                                          static_cast<unsigned char>(octreeLevel),
				                                                                          CCCoreLib::CloudSamplingTools::NEAREST_POINT_TO_CELL_CENTER,
				                                                                          progressDialog.data(),
				                                                                          octree);

				if (!refCloud)
				{
					return cmd.error(QObject::tr("Subsampling process failed!"));
				}
				cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));

				// save output
				result = desc.pc->partialClone(refCloud);
				delete refCloud;
				refCloud = nullptr;
			}
			else
			{
				cmd.print("\tNot subsampled, point count is smaller than max number of points");
				// no subsampling happened so result="input cloud"
				result = desc.pc;
				// set octreeLevel to indicate it was not subsampled at all
				octreeLevel = -1;
			}

			if (result)
			{
				result->setName(desc.pc->getName() + QObject::tr(".subsampled"));
				QString suffix = QObject::tr("OCTREE_LEVEL_%1_SUBSAMPLED").arg(octreeLevel);
				if (cmd.autoSaveMode())
				{
					CLCloudDesc newDesc(result, desc.basename, desc.path, desc.indexInFile);
					QString     errorStr = cmd.exportEntity(newDesc, suffix);
					if (!errorStr.isEmpty())
					{
						delete result;
						return cmd.error(errorStr);
					}
				}
				if (desc.pc != result)
				{
					// replace current cloud by subsampled one if it was changed
					delete desc.pc;
					desc.pc = result;
					desc.basename += '_' + suffix;
				}
			}
			else
			{
				return cmd.error(QObject::tr("Not enough memory!"));
			}
		}

		if (progressDialog)
		{
			progressDialog->close();
			QCoreApplication::processEvents();
		}
	}
	else
	{
		return cmd.error(QObject::tr("Unknown method!"));
	}

	return true;
}

CommandExtractCCs::CommandExtractCCs()
    : ccCommandLineInterface::Command(QObject::tr("ExtractCCs"), COMMAND_EXTRACT_CC)
{
}

bool CommandExtractCCs::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud loaded (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_EXTRACT_CC));
	}

	// octree level
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: octree level after \"-%1\"").arg(COMMAND_EXTRACT_CC));
	}
	bool          ok;
	unsigned char octreeLevel = std::min<unsigned char>(cmd.arguments().takeFirst().toUShort(&ok), CCCoreLib::DgmOctree::MAX_OCTREE_LEVEL);
	if (!ok)
	{
		return cmd.error(QObject::tr("Invalid octree level!"));
	}
	cmd.print(QObject::tr("\tOctree level: %1").arg(octreeLevel));

	// min number of points
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: minimum number of points per component after \"-%1 [octree level]\"").arg(COMMAND_EXTRACT_CC));
	}
	unsigned minPointCount = cmd.arguments().takeFirst().toUInt(&ok);
	if (!ok)
	{
		return cmd.error(QObject::tr("Invalid min. number of points!"));
	}
	cmd.print(QObject::tr("\tMin number of points per component: %1").arg(minPointCount));

	try
	{
		QScopedPointer<ccProgressDialog> progressDialog(nullptr);
		if (!cmd.silentMode())
		{
			progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
			progressDialog->setAutoClose(false);
		}

		std::vector<CLCloudDesc> inputClouds = cmd.clouds();
		cmd.clouds().clear();
		for (CLCloudDesc& desc : inputClouds)
		{
			cmd.print(QObject::tr("\tProcessing cloud %1").arg(!desc.pc->getName().isEmpty() ? desc.pc->getName() : "no name"));

			// we create/activate CCs label scalar field
			int sfIdx = desc.pc->getScalarFieldIndexByName(CC_CONNECTED_COMPONENTS_DEFAULT_LABEL_NAME);
			if (sfIdx < 0)
			{
				sfIdx = desc.pc->addScalarField(CC_CONNECTED_COMPONENTS_DEFAULT_LABEL_NAME);
			}
			if (sfIdx < 0)
			{
				cmd.error(QObject::tr("Couldn't allocate a new scalar field for computing CC labels! Try to free some memory ..."));
				continue;
			}
			desc.pc->setCurrentScalarField(sfIdx);

			// try to label all CCs
			int componentCount = CCCoreLib::AutoSegmentationTools::labelConnectedComponents(desc.pc,
			                                                                                static_cast<unsigned char>(octreeLevel),
			                                                                                false,
			                                                                                progressDialog.data());

			if (componentCount == 0)
			{
				cmd.error(QObject::tr("No component found!"));
				continue;
			}

			desc.pc->getCurrentInScalarField()->computeMinAndMax();
			CCCoreLib::ReferenceCloudContainer components;
			bool                               success = CCCoreLib::AutoSegmentationTools::extractConnectedComponents(desc.pc, components);
			desc.pc->deleteScalarField(sfIdx);
			sfIdx = -1;

			if (!success)
			{
				cmd.warning(QObject::tr("An error occurred (failed to finish the extraction)"));
				continue;
			}

			// we create "real" point clouds for all input components
			int realIndex = 0;
			for (size_t j = 0; j < components.size(); ++j)
			{
				CCCoreLib::ReferenceCloud* compIndexes = components[j];

				// if it has enough points
				if (compIndexes->size() >= minPointCount)
				{
					// we create a new entity
					ccPointCloud* compCloud = desc.pc->partialClone(compIndexes);
					if (compCloud)
					{
						//'shift on load' information
						compCloud->copyGlobalShiftAndScale(*desc.pc);
						compCloud->setName(QString(desc.pc->getName() + "_CC#%1").arg(j + 1));

						QString filenameSuffix = QObject::tr("_COMPONENT_%1").arg(++realIndex);
						if (desc.indexInFile >= 0)
						{
							// add the cloud name and its index in the file to avoid overwriting files if mutlitple clouds came from the same file
							filenameSuffix.prepend(QObject::tr("_CLOUD_%1(%2)").arg(desc.pc->getName()).arg(desc.indexInFile));
						}
						CLCloudDesc newDesc(compCloud, desc.basename + filenameSuffix, desc.path);
						if (cmd.autoSaveMode())
						{
							QString errorStr = cmd.exportEntity(newDesc, QString(), nullptr, ccCommandLineInterface::ExportOption::ForceNoTimestamp);
							if (!errorStr.isEmpty())
							{
								cmd.error(errorStr);
							}
						}
						// add newDesc to the current pool
						cmd.clouds().push_back(newDesc);
					}
					else
					{
						cmd.warning(QObject::tr("Failed to create component #%1! (not enough memory)").arg(j + 1));
					}
				}

				delete compIndexes;
				compIndexes = nullptr;
			}

			components.clear();

			if (cmd.clouds().empty())
			{
				cmd.error(QObject::tr("No component was created! Check the minimum size..."));
			}
			else
			{
				cmd.print(QObject::tr("%1 component(s) were created").arg(cmd.clouds().size()));
			}
		}

		if (progressDialog)
		{
			progressDialog->close();
			QCoreApplication::processEvents();
		}
	}
	catch (const std::bad_alloc&)
	{
		cmd.error(QObject::tr("Not enough memory"));
		return false;
	}

	return true;
}

CommandCurvature::CommandCurvature()
    : ccCommandLineInterface::Command(QObject::tr("Curvature"), COMMAND_CURVATURE)
{
}

bool CommandCurvature::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: curvature type after \"-%1\"").arg(COMMAND_CURVATURE));
	}

	QString                                 curvTypeStr = cmd.arguments().takeFirst().toUpper();
	CCCoreLib::Neighbourhood::CurvatureType curvType    = CCCoreLib::Neighbourhood::MEAN_CURV;
	if (curvTypeStr == "MEAN")
	{
		// curvType = CCCoreLib::Neighbourhood::MEAN_CURV;
	}
	else if (curvTypeStr == "GAUSS")
	{
		curvType = CCCoreLib::Neighbourhood::GAUSSIAN_CURV;
	}
	else if (curvTypeStr == "NORMAL_CHANGE")
	{
		curvType = CCCoreLib::Neighbourhood::NORMAL_CHANGE_RATE;
	}
	else
	{
		return cmd.error(QObject::tr("Invalid curvature type after \"-%1\". Got '%2' instead of MEAN or GAUSS.").arg(COMMAND_CURVATURE, curvTypeStr));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: kernel size after curvature type"));
	}

	bool                paramOk    = false;
	QString             kernelStr  = cmd.arguments().takeFirst();
	PointCoordinateType kernelSize = static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
	if (!paramOk)
	{
		return cmd.error(QObject::tr("Failed to read a numerical parameter: kernel size (after curvature type). Got '%1' instead.").arg(kernelStr));
	}
	cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute curvature! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_CURVATURE));
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	if (ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::Curvature, curvType, kernelSize, entities, nullptr, cmd.widgetParent()))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds(QObject::tr("%1_CURVATURE_KERNEL_%2").arg(curvTypeStr).arg(kernelSize)))
		{
			return false;
		}
	}
	return true;
}

static bool ReadDensityType(ccCommandLineInterface& cmd, CCCoreLib::GeometricalAnalysisTools::Density& density)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: density type after \"-%1\" (KNN/SURFACE/VOLUME)").arg(COMMAND_DENSITY_TYPE));
	}

	// read option confirmed, we can move on
	QString typeArg = cmd.arguments().takeFirst().toUpper();
	if (typeArg == "KNN")
	{
		density = CCCoreLib::GeometricalAnalysisTools::DENSITY_KNN;
	}
	else if (typeArg == "SURFACE")
	{
		density = CCCoreLib::GeometricalAnalysisTools::DENSITY_2D;
	}
	else if (typeArg == "VOLUME")
	{
		density = CCCoreLib::GeometricalAnalysisTools::DENSITY_3D;
	}
	else
	{
		return cmd.error(QObject::tr("Invalid parameter: density type is expected after \"-%1\" (KNN/SURFACE/VOLUME)").arg(COMMAND_DENSITY_TYPE));
	}

	return true;
}

CommandApproxDensity::CommandApproxDensity()
    : ccCommandLineInterface::Command(QObject::tr("Approx Density"), COMMAND_APPROX_DENSITY)
{
}

bool CommandApproxDensity::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute approx. density! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_APPROX_DENSITY));
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	// optional parameter: density type
	CCCoreLib::GeometricalAnalysisTools::Density densityType = CCCoreLib::GeometricalAnalysisTools::DENSITY_3D;
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_DENSITY_TYPE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: density type after \"-%1\" (KNN/SURFACE/VOLUME)").arg(COMMAND_DENSITY_TYPE));
			}
			// read option confirmed, we can move on
			if (!ReadDensityType(cmd, densityType))
			{
				return false;
			}
		}
	}

	if (ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::ApproxLocalDensity, densityType, 0, entities, nullptr, cmd.widgetParent()))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds("APPROX_DENSITY"))
		{
			return false;
		}
	}

	return true;
}

CommandDensity::CommandDensity()
    : ccCommandLineInterface::Command(QObject::tr("Density"), COMMAND_DENSITY)
{
}

bool CommandDensity::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: sphere radius after \"-%1\"").arg(COMMAND_DENSITY));
	}

	bool                paramOk    = false;
	QString             kernelStr  = cmd.arguments().takeFirst();
	PointCoordinateType kernelSize = static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
	if (!paramOk)
	{
		return cmd.error(QObject::tr("Failed to read a numerical parameter: sphere radius (after \"-%1\"). Got '%2' instead.").arg(COMMAND_DENSITY, kernelStr));
	}
	cmd.print(QObject::tr("\tSphere radius: %1").arg(kernelSize));

	// optional parameter: density type
	CCCoreLib::GeometricalAnalysisTools::Density densityType = CCCoreLib::GeometricalAnalysisTools::DENSITY_3D;
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_DENSITY_TYPE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: density type after \"-%1\" (KNN/SURFACE/VOLUME)").arg(COMMAND_DENSITY_TYPE));
			}
			// read option confirmed, we can move on
			if (!ReadDensityType(cmd, densityType))
			{
				return false;
			}
		}
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute density! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_DENSITY));
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	if (ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::LocalDensity, densityType, kernelSize, entities, nullptr, cmd.widgetParent()))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds("DENSITY"))
		{
			return false;
		}
	}

	return true;
}

CommandSFGradient::CommandSFGradient()
    : ccCommandLineInterface::Command(QObject::tr("SF gradient"), COMMAND_SF_GRADIENT)
{
}

bool CommandSFGradient::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: boolean (whether SF is euclidean or not) after \"-%1\"").arg(COMMAND_SF_GRADIENT));
	}

	QString euclideanStr = cmd.arguments().takeFirst().toUpper();
	bool    euclidean    = false;
	if (euclideanStr == "TRUE")
	{
		euclidean = true;
	}
	else if (euclideanStr != "FALSE")
	{
		return cmd.error(QObject::tr("Invalid boolean value after \"-%1\". Got '%2' instead of TRUE or FALSE.").arg(COMMAND_SF_GRADIENT, euclideanStr));
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute SF gradient! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SF_GRADIENT));
	}

	// Call MainWindow generic method
	void*                additionalParameters[1] = {&euclidean};
	ccHObject::Container entities;
	entities.reserve(cmd.clouds().size());
	for (CLCloudDesc& desc : cmd.clouds())
	{
		unsigned sfCount = desc.pc->getNumberOfScalarFields();
		if (sfCount == 0)
		{
			cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no scalar field (it will be ignored)").arg(desc.pc->getName()));
		}
		else
		{
			if (sfCount > 1)
			{
				cmd.warning(QObject::tr("cmd.warning: cloud '%1' has several scalar fields (the active one will be used by default, or the first one if none is active)").arg(desc.pc->getName()));
			}

			int activeSFIndex = desc.pc->getCurrentOutScalarFieldIndex();
			if (activeSFIndex < 0)
			{
				activeSFIndex = 0;
			}

			desc.pc->setCurrentDisplayedScalarField(activeSFIndex);

			entities.push_back(desc.pc);
		}
	}

	if (ccLibAlgorithms::ApplyCCLibAlgorithm(ccLibAlgorithms::CCLIB_ALGO_SF_GRADIENT, entities, cmd.widgetParent(), additionalParameters))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds(euclidean ? "EUCLIDEAN_SF_GRAD" : "SF_GRAD"))
		{
			return false;
		}
	}

	return true;
}

CommandRoughness::CommandRoughness()
    : ccCommandLineInterface::Command(QObject::tr("Roughness"), COMMAND_ROUGHNESS)
{
}

bool CommandRoughness::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: kernel size after \"-%1\"").arg(COMMAND_ROUGHNESS));
	}

	bool                paramOk    = false;
	QString             kernelStr  = cmd.arguments().takeFirst();
	PointCoordinateType kernelSize = static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
	if (!paramOk)
	{
		return cmd.error(QObject::tr("Failed to read a numerical parameter: kernel size (after \"-%1\"). Got '%2' instead.").arg(COMMAND_ROUGHNESS, kernelStr));
	}
	cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));

	// optional argument
	CCVector3  roughnessUpDir;
	CCVector3* _roughnessUpDir = nullptr;
	if (cmd.arguments().size() >= 4)
	{
		QString nextArg = cmd.arguments().first();
		if (nextArg.startsWith('-') && nextArg.mid(1).toUpper() == COMMAND_ROUGHNESS_UP_DIR)
		{
			// option confirmed
			cmd.arguments().takeFirst();
			QString xStr = cmd.arguments().takeFirst();
			QString yStr = cmd.arguments().takeFirst();
			QString zStr = cmd.arguments().takeFirst();
			bool    okX = false, okY = false, okZ = false;
			roughnessUpDir.x = static_cast<PointCoordinateType>(xStr.toDouble(&okX));
			roughnessUpDir.y = static_cast<PointCoordinateType>(yStr.toDouble(&okY));
			roughnessUpDir.z = static_cast<PointCoordinateType>(zStr.toDouble(&okZ));
			if (!okX || !okY || !okZ)
			{
				return cmd.error(QObject::tr("Invalid 'up direction' vector after option -%1 (3 coordinates expected)").arg(COMMAND_ROUGHNESS_UP_DIR));
			}
			_roughnessUpDir = &roughnessUpDir;
		}
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute roughness! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_ROUGHNESS));
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	if (ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::Roughness, 0, kernelSize, entities, _roughnessUpDir, cmd.widgetParent()))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds(QObject::tr("ROUGHNESS_KERNEL_%2").arg(kernelSize)))
		{
			return false;
		}
	}

	return true;
}

CommandApplyTransformation::CommandApplyTransformation()
    : ccCommandLineInterface::Command(QObject::tr("Apply Transformation"), COMMAND_APPLY_TRANSFORMATION)
{
}

bool CommandApplyTransformation::process(ccCommandLineInterface& cmd)
{
	// optional parameters
	bool inverse            = false;
	bool applyToGlobal      = false;
	bool forceApplyToGlobal = false;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_APPLY_TRANS_INVERSE))
		{
			// local option confirmed, we can move on
			inverse = true;
			cmd.arguments().pop_front();
			cmd.print("Transformation matrix will be inversed");
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_APPLY_TRANS_TO_GLOBAL))
		{
			// local option confirmed, we can move on
			applyToGlobal = true;
			cmd.arguments().pop_front();
			cmd.print("Transformation will be applied to the global coordinates (Global Shift may be automatically adjusted to preserve accuracy)");

			// look for the 'FORCE' sub-option
			if (!cmd.arguments().empty() && cmd.arguments().front().toUpper() == OPTION_FORCE)
			{
				// local option confirmed, we can move on
				forceApplyToGlobal = true;
				cmd.arguments().pop_front();
				cmd.print("Transformation will be applied to the global coordinates even if the entity already has large coordinates");
			}
		}
		else
		{
			break;
		}
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: transformation file after \"-%1\"").arg(COMMAND_APPLY_TRANSFORMATION));
	}

	QString     filename = cmd.arguments().takeFirst();
	ccGLMatrixd mat;
	if (!mat.fromAsciiFile(filename))
	{
		return cmd.error(QObject::tr("Failed to read transformation matrix file '%1'!").arg(filename));
	}
	if (inverse)
	{
		cmd.print(QObject::tr("Transformation before inversion:\n") + mat.toString());
		mat = mat.inverse();
	}

	cmd.print(QObject::tr("Transformation:\n") + mat.toString());

	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No entity on which to apply the transformation! (be sure to open one with \"-%1 [filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_APPLY_TRANSFORMATION));
	}

	// create an entity vector
	size_t                                                  nrOfClouds   = cmd.clouds().size();
	size_t                                                  nrOfMeshes   = cmd.meshes().size();
	size_t                                                  nrOfEntities = nrOfClouds + nrOfMeshes;
	std::vector<std::pair<ccShiftedObject*, CLEntityDesc*>> entities;
	entities.reserve(nrOfEntities);

	// add clouds to the vector
	for (CLCloudDesc& desc : cmd.clouds())
	{
		entities.push_back({desc.pc, &desc});
	}

	// add meshes to the vector
	for (CLMeshDesc& desc : cmd.meshes())
	{
		bool             isLocked = false;
		ccShiftedObject* shifted  = ccHObjectCaster::ToShifted(desc.mesh, &isLocked);
		if (shifted && !isLocked)
		{
			entities.push_back({shifted, &desc});
		}
	}

	// if the transformation is partly converted to global shift/scale
	bool       autoApplyPreviousGlobalShiftAndScale = false;
	double     previousScale                        = 1.0;
	CCVector3d previousShift(0, 0, 0);

	// process both clouds and meshes
	for (size_t i = 0; i < nrOfEntities; i++)
	{
		CLEntityDesc&    desc          = *entities[i].second;
		ccShiftedObject* shiftedEntity = entities[i].first;

		ccGLMatrixd transMat(mat);
		if (applyToGlobal)
		{
			// the user wants to apply the transformation to the global coordinates
			CCVector3d globalShift = shiftedEntity->getGlobalShift();
			double     globalScale = shiftedEntity->getGlobalScale();

			// we compute the impact to the local coordinate system without changing the
			// actual Global Shift & Scale parameters (for now)
			CCVector3d localTranslation = globalScale * (globalShift - transMat * globalShift + 2 * transMat.getTranslationAsVec3D());

			// we switch to a local transformation matrix
			transMat.setTranslation(localTranslation);

			// test if the translated cloud coordinates were already "too large"
			ccBBox     localBBox = shiftedEntity->getOwnBB();
			CCVector3d Pl        = localBBox.minCorner();
			double     Dl        = localBBox.getDiagNormd();
			if (forceApplyToGlobal || (!ccGlobalShiftManager::NeedShift(Pl) && !ccGlobalShiftManager::NeedRescale(Dl)))
			{
				// test if the translated (local) cloud coordinates are too large
				ccBBox     transformedLocalBox = localBBox * transMat;
				CCVector3d transformedPl       = transformedLocalBox.minCorner();
				double     transformedDl       = transformedLocalBox.getDiagNormd();

				bool needShift = ccGlobalShiftManager::NeedShift(transformedPl) || ccGlobalShiftManager::NeedRescale(transformedDl);
				if (needShift)
				{
					// existing shift information
					CCVector3d globalShift = shiftedEntity->getGlobalShift();
					double     globalScale = shiftedEntity->getGlobalScale();

					// we compute the global coordinates and scale of the reference point (= the min corner of the bounding-box)
					CCVector3d Pg = shiftedEntity->toGlobal3d(transformedPl);
					double     Dg = transformedDl / globalScale;

					// let's try to find better Global Shift and Scale values
					CCVector3d newShift(0.0, 0.0, 0.0);
					double     newScale = 1.0;

					// should we try to use the previous Global Shift and Scale values?
					if (autoApplyPreviousGlobalShiftAndScale)
					{
						if (!ccGlobalShiftManager::NeedShift(Pg + previousShift) && !ccGlobalShiftManager::NeedRescale(Dg * previousScale))
						{
							newScale  = previousScale;
							newShift  = previousShift;
							needShift = false;
						}
					}

					if (needShift)
					{
						// don't bother the user as we are running in command line, use the best possible solution
						newShift  = ccGlobalShiftManager::BestShift(Pg);
						newScale  = ccGlobalShiftManager::BestScale(Dg);
						needShift = false;
						// force this shift for upcoming entities
						autoApplyPreviousGlobalShiftAndScale = true;
						previousShift                        = newShift;
						previousScale                        = newScale;
					}

					// get the relative modification to existing global shift/scale info
					double     scaleChange = newScale / globalScale;
					CCVector3d shiftChange = newShift - globalShift;

					if (scaleChange != 1.0 || shiftChange.norm2() != 0)
					{
						// apply translation as global shift
						shiftedEntity->setGlobalShift(newShift);
						shiftedEntity->setGlobalScale(newScale);
						cmd.warning(QObject::tr("Entity '%1' global shift/scale information has been updated: shift = (%2,%3,%4) / scale = %5")
						                .arg(shiftedEntity->getName())
						                .arg(newShift.x)
						                .arg(newShift.y)
						                .arg(newShift.z)
						                .arg(newScale));

						transMat.scaleRotation(scaleChange);
						transMat.setTranslation(transMat.getTranslationAsVec3D() + newScale * shiftChange);
					}
				}
			}
			else
			{
				cmd.warning(QObject::tr("Entity '%1' already has very large local coordinates. Global shift/scale won't be automatically adjusted to preserve accuracy. Consider using the -%2 option to force global shift/scale adjustment.").arg(shiftedEntity->getName()).arg(OPTION_FORCE));
			}
		}
		else
		{
			// check if the transformed coordinates are too large
			ccBBox     transformedLocalBox = shiftedEntity->getOwnBB() * transMat;
			CCVector3d transformedPl       = transformedLocalBox.minCorner();
			double     transformedDl       = transformedLocalBox.getDiagNormd();
			bool       needShift           = ccGlobalShiftManager::NeedShift(transformedPl) || ccGlobalShiftManager::NeedRescale(transformedDl);
			if (needShift)
			{
				cmd.warning(QObject::tr("Entity '%1' will have very large local coordinates after transformation. Consider using the -%1 option to preserve accuracy.").arg(COMMAND_APPLY_TRANS_TO_GLOBAL));
			}
		}

		ccGLMatrix matrixToApply(transMat.data());
		shiftedEntity->applyGLTransformation_recursive(&matrixToApply);
		QString nameSuffix = "_TRANSFORMED";
		if ((&desc)->getCLEntityType() == CL_ENTITY_TYPE::MESH)
		{
			// set the mesh name instead of the vertices cloud inside the mesh
			ccHObject* parent = shiftedEntity->getParent();
			if (parent)
			{
				parent->setName(QObject::tr("%1%2").arg(parent->getName()).arg(nameSuffix));
			}
		}
		else
		{
			shiftedEntity->setName(QObject::tr("%1%2").arg(shiftedEntity->getName()).arg(nameSuffix));
		}
		desc.basename += nameSuffix;

		// save it as well
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(desc);
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}
	return true;
}

CommandDropGlobalShift::CommandDropGlobalShift()
    : ccCommandLineInterface::Command(QObject::tr("Drop global shift"), COMMAND_DROP_GLOBAL_SHIFT)
{
}

bool CommandDropGlobalShift::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No loaded entity! (be sure to open one with \"-%1 [filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_DROP_GLOBAL_SHIFT));
	}

	// process clouds
	for (const CLCloudDesc& desc : cmd.clouds())
	{
		desc.pc->setGlobalShift(0, 0, 0);
	}

	for (const CLMeshDesc& desc : cmd.meshes())
	{
		bool             isLocked = false;
		ccShiftedObject* shifted  = ccHObjectCaster::ToShifted(desc.mesh, &isLocked);
		if (shifted && !isLocked)
		{
			shifted->setGlobalShift(0, 0, 0);
		}
	}

	return true;
}

CommandSFColorScale::CommandSFColorScale()
    : ccCommandLineInterface::Command(QObject::tr("SF color scale"), COMMAND_SF_COLOR_SCALE)
{
}

bool CommandSFColorScale::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: color scale file after \"-%1\"").arg(COMMAND_SF_COLOR_SCALE));
	}

	QString filename = cmd.arguments().takeFirst();

	ccColorScale::Shared scale = ccColorScale::LoadFromXML(filename);

	if (!scale)
	{
		return cmd.error(QObject::tr("Failed to read color scale file '%1'!").arg(filename));
	}

	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No point cloud or mesh on which to set the SF color scale! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SF_COLOR_SCALE));
	}

	// clouds
	if (!cmd.clouds().empty())
	{
		bool hasCandidateClouds = false;
		for (auto& cloud : cmd.clouds())
		{
			ccScalarField* sf = static_cast<ccScalarField*>(cloud.pc->getCurrentOutScalarField());
			if (sf)
			{
				sf->setColorScale(scale);
				hasCandidateClouds = true;
			}
		}

		if (hasCandidateClouds && cmd.autoSaveMode() && !cmd.saveClouds("COLOR_SCALE"))
		{
			return false;
		}
	}

	// meshes
	if (!cmd.meshes().empty())
	{
		bool hasCandidateMeshes = false;
		for (auto& mesh : cmd.meshes())
		{
			ccPointCloud* vertices = dynamic_cast<ccPointCloud*>(mesh.mesh->getAssociatedCloud());
			if (vertices)
			{
				ccScalarField* sf = static_cast<ccScalarField*>(vertices->getCurrentOutScalarField());
				if (sf)
				{
					sf->setColorScale(scale);
					hasCandidateMeshes = true;
				}
			}
		}

		if (hasCandidateMeshes && cmd.autoSaveMode() && !cmd.saveMeshes("COLOR_SCALE"))
		{
			return false;
		}
	}

	return true;
}

CommandSFConvertToRGB::CommandSFConvertToRGB()
    : ccCommandLineInterface::Command(QObject::tr("SF convert to RGB"), COMMAND_SF_CONVERT_TO_RGB)
{
}

bool CommandSFConvertToRGB::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: boolean (whether to mix with existing colors or not) after \"-%1\"").arg(COMMAND_SF_CONVERT_TO_RGB));
	}

	QString mixWithExistingColorsStr = cmd.arguments().takeFirst().toUpper();
	bool    mixWithExistingColors    = false;
	if (mixWithExistingColorsStr == "TRUE")
	{
		mixWithExistingColors = true;
	}
	else if (mixWithExistingColorsStr != "FALSE")
	{
		return cmd.error(QObject::tr("Invalid boolean value after \"-%1\". Got '%2' instead of TRUE or FALSE.").arg(COMMAND_SF_CONVERT_TO_RGB, mixWithExistingColorsStr));
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to convert SF to RGB! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SF_CONVERT_TO_RGB));
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		unsigned sfCount       = desc.pc->getNumberOfScalarFields();
		int      activeSFIndex = desc.pc->getCurrentOutScalarFieldIndex();

		if (sfCount == 0)
		{
			cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no scalar field (it will be ignored)").arg(desc.pc->getName()));
		}
		else if (activeSFIndex < 0)
		{
			cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no active scalar field (it will be ignored)").arg(desc.pc->getName()));
		}
		else
		{
			int displaySFIndex = desc.pc->getCurrentDisplayedScalarFieldIndex();
			desc.pc->setCurrentDisplayedScalarField(activeSFIndex);

			if (desc.pc->convertCurrentScalarFieldToColors(mixWithExistingColors))
			{
				desc.pc->showColors(true);
				desc.pc->showSF(false);
			}
			else
			{
				cmd.warning(QObject::tr("cmd.warning: cloud '%1' failed to convert SF to RGB").arg(desc.pc->getName()));
			}

			desc.pc->setCurrentDisplayedScalarField(displaySFIndex);
		}
	}

	if (cmd.autoSaveMode() && !cmd.saveClouds("SF_CONVERT_TO_RGB"))
	{
		return false;
	}

	return true;
}

CommandRGBConvertToSF::CommandRGBConvertToSF()
    : ccCommandLineInterface::Command(QObject::tr("RGB convert to SF"), COMMAND_RGB_CONVERT_TO_SF)
{
}

bool CommandRGBConvertToSF::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to convert RGB to SF! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_RGB_CONVERT_TO_SF));
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (!desc.pc->hasColors())
		{
			cmd.warning(QObject::tr("Cloud %1 has no colors").arg(desc.pc->getName()));
			continue;
		}

		ccHObject::Container container;
		container.push_back(desc.pc);
		if (!ccEntityAction::sfFromColor(container, /*exportR=*/true, /*exportG=*/true, /*exportB=*/true, /*exportAlpha=*/true, /*exportC=*/true)) // beta version, only composite
		{
			return cmd.error(QObject::tr("Failed to convert RGB to scalar fields"));
		}

		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(desc, "_RGB_TO_SF");
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandFilterBySFValue::CommandFilterBySFValue()
    : ccCommandLineInterface::Command(QObject::tr("Filter by SF value"), COMMAND_FILTER_SF_BY_VALUE)
{
}

// special SF values that can be used instead of explicit ones
enum USE_SPECIAL_SF_VALUE
{
	USE_NONE,
	USE_MIN,
	USE_DISP_MIN,
	USE_SAT_MIN,
	USE_N_SIGMA_MIN,
	USE_MAX,
	USE_DISP_MAX,
	USE_SAT_MAX,
	USE_N_SIGMA_MAX
};

static std::pair<ScalarType, ScalarType> GetSFRange(const CCCoreLib::ScalarField& sf,
                                                    ScalarType                    minVal,
                                                    USE_SPECIAL_SF_VALUE          useValForMin,
                                                    ScalarType                    maxVal,
                                                    USE_SPECIAL_SF_VALUE          useValForMax)
{
	ScalarType thisMinVal = minVal;
	{
		switch (useValForMin)
		{
		case USE_MIN:
			thisMinVal = sf.getMin();
			break;
		case USE_DISP_MIN:
			thisMinVal = static_cast<const ccScalarField&>(sf).displayRange().start();
			break;
		case USE_SAT_MIN:
			thisMinVal = static_cast<const ccScalarField&>(sf).saturationRange().start();
			break;
		case USE_N_SIGMA_MIN:
			ScalarType mean;
			ScalarType variance;
			sf.computeMeanAndVariance(mean, &variance);
			thisMinVal = mean - (sqrt(variance) * minVal);
			break;
		default:
			// nothing to do
			break;
		}
	}

	ScalarType thisMaxVal = maxVal;
	{
		switch (useValForMax)
		{
		case USE_MAX:
			thisMaxVal = sf.getMax();
			break;
		case USE_DISP_MAX:
			thisMaxVal = static_cast<const ccScalarField&>(sf).displayRange().stop();
			break;
		case USE_SAT_MAX:
			thisMaxVal = static_cast<const ccScalarField&>(sf).saturationRange().stop();
			break;
		case USE_N_SIGMA_MAX:
			ScalarType mean;
			ScalarType variance;
			sf.computeMeanAndVariance(mean, &variance);
			thisMaxVal = mean + (sqrt(variance) * maxVal);
			break;
		default:
			// nothing to do
			break;
		}
	}

	return {thisMinVal, thisMaxVal};
}

static ScalarType GetSFValue(const ccPointCloud& pc, int sfIndex, ScalarType value, USE_SPECIAL_SF_VALUE useVal)
{
	CCCoreLib::ScalarField* sf = pc.getScalarField(sfIndex);
	// should be handled way before this point this is just safety
	if (sf)
	{
		std::pair<ScalarType, ScalarType> range = GetSFRange(*sf, value, useVal, value, useVal);
		if (useVal <= USE_N_SIGMA_MIN)
		{
			return range.first;
		}
		else
		{
			return range.second;
		}
	}
	return 1.0;
}

static USE_SPECIAL_SF_VALUE ToSpecialSFValue(QString valString)
{
	valString = valString.toUpper();

	if (valString == "MIN")
	{
		return USE_MIN;
	}
	else if (valString == "DISP_MIN")
	{
		return USE_DISP_MIN;
	}
	else if (valString == "SAT_MIN")
	{
		return USE_SAT_MIN;
	}
	else if (valString == "N_SIGMA_MIN")
	{
		return USE_N_SIGMA_MIN;
	}
	else if (valString == "MAX")
	{
		return USE_MAX;
	}
	else if (valString == "DISP_MAX")
	{
		return USE_DISP_MAX;
	}
	else if (valString == "SAT_MAX")
	{
		return USE_SAT_MAX;
	}
	else if (valString == "N_SIGMA_MAX")
	{
		return USE_N_SIGMA_MAX;
	}
	else
	{
		return USE_NONE;
	}
}

bool CommandFilterBySFValue::process(ccCommandLineInterface& cmd)
{
	USE_SPECIAL_SF_VALUE useValForMin = USE_NONE;
	ScalarType           minVal       = 0;
	QString              minValStr;
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: min value after \"-%1\"").arg(COMMAND_FILTER_SF_BY_VALUE));
		}

		bool paramOk = false;
		minValStr    = cmd.arguments().takeFirst();

		useValForMin = ToSpecialSFValue(minValStr);

		if (useValForMin == USE_N_SIGMA_MIN)
		{
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: N value (after \"-%1 N_SIGMA_MIN\").").arg(COMMAND_FILTER_SF_BY_VALUE));
			}
			minValStr = cmd.arguments().takeFirst();
			minVal    = static_cast<ScalarType>(minValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: N value (after \"N_SIGMA_MIN\"). Got '%2' instead.").arg(minValStr));
			}
		}
		else if (useValForMin == USE_N_SIGMA_MAX)
		{
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: N value (after \"-%1 N_SIGMA_MAX\").").arg(COMMAND_FILTER_SF_BY_VALUE));
			}
			minValStr = cmd.arguments().takeFirst();
			minVal    = static_cast<ScalarType>(minValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: N value (after \"N_SIGMA_MAX\"). Got '%2' instead.").arg(minValStr));
			}
		}
		else if (useValForMin == USE_NONE)
		{
			minVal = static_cast<ScalarType>(minValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: min value (after \"-%1\"). Got '%2' instead.").arg(COMMAND_FILTER_SF_BY_VALUE, minValStr));
			}
		}
	}

	USE_SPECIAL_SF_VALUE useValForMax = USE_NONE;
	ScalarType           maxVal       = 0;
	QString              maxValStr;
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: max value after \"-%1\" {min}").arg(COMMAND_FILTER_SF_BY_VALUE));
		}

		bool paramOk = false;
		maxValStr    = cmd.arguments().takeFirst();

		useValForMax = ToSpecialSFValue(maxValStr);

		if (useValForMax == USE_N_SIGMA_MIN)
		{
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: N value (after \"-%1 XXX N_SIGMA_MIN\").").arg(COMMAND_FILTER_SF_BY_VALUE));
			}
			maxValStr = cmd.arguments().takeFirst();
			maxVal    = static_cast<ScalarType>(maxValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: N value (after \"N_SIGMA_MIN\"). Got '%2' instead.").arg(maxValStr));
			}
		}
		else if (useValForMax == USE_N_SIGMA_MAX)
		{
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: N value (after \"-%1 XXX N_SIGMA_MAX\").").arg(COMMAND_FILTER_SF_BY_VALUE));
			}
			maxValStr = cmd.arguments().takeFirst();
			maxVal    = static_cast<ScalarType>(maxValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: N value (after \"N_SIGMA_MAX\"). Got '%2' instead.").arg(maxValStr));
			}
		}
		else if (useValForMax == USE_NONE)
		{
			maxVal = static_cast<ScalarType>(maxValStr.toDouble(&paramOk));
			if (!paramOk)
			{
				return cmd.error(QObject::tr("Failed to read a numerical parameter: max value (after min value). Got '%1' instead.").arg(COMMAND_FILTER_SF_BY_VALUE, maxValStr));
			}
		}
	}

	cmd.print(QObject::tr("\tInterval: [%1 - %2]").arg(minValStr, maxValStr));

	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No point cloud nor mesh on which to filter SF! (be sure to open one or generate one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_FILTER_SF_BY_VALUE));
	}

	// for each cloud
	for (CLCloudDesc& desc : cmd.clouds())
	{
		CCCoreLib::ScalarField* sf = desc.pc->getCurrentOutScalarField();
		if (sf)
		{
			std::pair<ScalarType, ScalarType> range = GetSFRange(*sf, minVal, useValForMin, maxVal, useValForMax);

			ccPointCloud* fitleredCloud = desc.pc->filterPointsByScalarValue(range.first, range.second);
			if (fitleredCloud)
			{
				cmd.print(QObject::tr("\t\tCloud '%1' --> %2/%3 points remaining").arg(desc.pc->getName()).arg(fitleredCloud->size()).arg(desc.pc->size()));

				if (fitleredCloud != desc.pc)
				{
					// replace current cloud by this one
					delete desc.pc;
					desc.pc = fitleredCloud;
				}
				desc.basename += QObject::tr("_FILTERED_[%1_%2]").arg(range.first).arg(range.second);

				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc);
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	// for each mesh
	for (CLMeshDesc& desc : cmd.meshes())
	{
		ccGenericMesh* mesh = desc.mesh;
		ccPointCloud*  pc   = ccHObjectCaster::ToPointCloud(mesh);
		if (!pc)
		{
			// strange mesh
			continue;
		}

		CCCoreLib::ScalarField* sf = pc->getCurrentOutScalarField();
		if (sf)
		{
			std::pair<ScalarType, ScalarType> range = GetSFRange(*sf, minVal, useValForMin, maxVal, useValForMax);

			pc->hidePointsByScalarValue(range.first, range.second);
			ccGenericMesh* filteredMesh = nullptr;
			if (mesh->isA(CC_TYPES::MESH) /*|| ent->isKindOf(CC_TYPES::PRIMITIVE)*/) // TODO
				filteredMesh = ccHObjectCaster::ToMesh(mesh)->createNewMeshFromSelection(false);
			else if (mesh->isA(CC_TYPES::SUB_MESH))
				filteredMesh = ccHObjectCaster::ToSubMesh(mesh)->createNewSubMeshFromSelection(false);
			else
			{
				cmd.warning("Unhandled mesh type for entitiy " + mesh->getName());
				continue;
			}

			if (filteredMesh)
			{
				cmd.print(QObject::tr("\t\tMesh '%1' --> %2/%3 triangles remaining").arg(mesh->getName()).arg(filteredMesh->size()).arg(mesh->size()));

				// replace current mesh by this one
				if (filteredMesh != mesh) // it's technically possible to have the same pointer if all triangles were filtered (with 'createNewMeshFromSelection')
				{
					delete mesh;
					mesh      = nullptr;
					desc.mesh = filteredMesh;
				}
				desc.basename += QObject::tr("_FILTERED_[%1_%2]").arg(range.first).arg(range.second);

				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc);
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	return true;
}

CommandComputeMeshVolume::CommandComputeMeshVolume()
    : ccCommandLineInterface::Command(QObject::tr("Compute mesh volume"), COMMAND_MESH_VOLUME)
{
}

bool CommandComputeMeshVolume::process(ccCommandLineInterface& cmd)
{
	if (cmd.meshes().empty())
	{
		cmd.warning(QObject::tr("No mesh loaded! Nothing to do..."));
		return true;
	}

	// optional parameters
	QString outputFilename;
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_VOLUME_TO_FILE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (!cmd.arguments().empty())
			{
				outputFilename = cmd.arguments().front();
				cmd.arguments().pop_front();
				cmd.print(QObject::tr("Volume report file: %1").arg(outputFilename));
			}
			else
			{
				return cmd.error(QObject::tr("Missing argument: filename after '%1'").arg(COMMAND_VOLUME_TO_FILE));
			}
		}
	}

	QFile       outFile;
	QTextStream outStream(&outFile);
	if (!outputFilename.isEmpty())
	{
		outFile.setFileName(outputFilename);
		if (!outFile.open(QFile::WriteOnly | QFile::Text))
		{
			return cmd.error(QObject::tr("Failed to create/open volume report file"));
		}
	}

	// for each mesh
	for (CLMeshDesc& desc : cmd.meshes())
	{
		// we compute the mesh volume
		double V = CCCoreLib::MeshSamplingTools::computeMeshVolume(desc.mesh);

		QString titleStr = QObject::tr("Mesh '%1'").arg(desc.basename);
		if (desc.indexInFile >= 0)
		{
			titleStr += QObject::tr(" (#%2)").arg(desc.indexInFile);
		}
		cmd.print(titleStr);
		QString volumeStr = QObject::tr("V = %2").arg(V, 0, 'f', 8);
		cmd.print(volumeStr);

		if (outFile.isOpen())
		{
			outStream << titleStr << endl;
			outStream << volumeStr << endl;
		}
	}

	return true;
}

CommandMergeMeshes::CommandMergeMeshes()
    : ccCommandLineInterface::Command(QObject::tr("Merge meshes"), COMMAND_MERGE_MESHES)
{
}

bool CommandMergeMeshes::process(ccCommandLineInterface& cmd)
{
	if (cmd.meshes().size() < 2)
	{
		cmd.warning(QObject::tr("Less than 2 meshes are loaded! Nothing to do..."));
		return true;
	}

	CLMeshDesc mergedMeshDesc;
	bool       firstValidMesh = true;

	// create the destination mesh
	ccPointCloud*          vertices = new ccPointCloud("vertices");
	QScopedPointer<ccMesh> mergedMesh(new ccMesh(vertices));
	mergedMesh->setName("Merged mesh");
	mergedMesh->addChild(vertices);
	vertices->setEnabled(false);

	// merge meshes
	for (CLMeshDesc& desc : cmd.meshes())
	{
		// get the mesh
		ccMesh* mesh = dynamic_cast<ccMesh*>(desc.mesh);
		if (!mesh)
		{
			ccLog::Error(QObject::tr("Can't merge mesh '%1' (unhandled type)").arg(desc.basename));
		}

		if (mergedMesh->merge(mesh, true)) // merge it
		{
			if (firstValidMesh)
			{
				// copy the first valid mesh description
				mergedMeshDesc      = desc;
				mergedMeshDesc.mesh = nullptr;
				firstValidMesh      = false;
			}
		}
		else
		{
			return cmd.error(QObject::tr("Merge operation failed"));
		}

		delete desc.mesh;
		desc.mesh = nullptr;
	}

	if (mergedMesh->size() == 0)
	{
		return cmd.error(QObject::tr("Result is empty"));
	}

	// clean the 'cmd.meshes()' vector
	cmd.removeMeshes();
	// add the new mesh
	mergedMeshDesc.basename += QObject::tr("_MERGED");
	mergedMeshDesc.mesh = mergedMesh.take();
	cmd.meshes().push_back(mergedMeshDesc);

	if (cmd.autoSaveMode())
	{
		QString errorStr = cmd.exportEntity(mergedMeshDesc);
		if (!errorStr.isEmpty())
		{
			return cmd.error(errorStr);
		}
	}

	return true;
}

CommandMergeClouds::CommandMergeClouds()
    : ccCommandLineInterface::Command(QObject::tr("Merge clouds"), COMMAND_MERGE_CLOUDS)
{
}

bool CommandMergeClouds::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().size() < 2)
	{
		cmd.warning(QObject::tr("Less than 2 clouds are loaded! Nothing to do..."));
		return true;
	}

	// merge clouds
	if (!cmd.clouds().empty())
	{
		for (size_t i = 1; i < cmd.clouds().size(); ++i)
		{
			unsigned beforePts = cmd.clouds().front().pc->size();

			CLCloudDesc& desc   = cmd.clouds()[i];
			unsigned     newPts = desc.pc->size();
			*cmd.clouds().front().pc += desc.pc;

			// success?
			if (cmd.clouds().front().pc->size() == beforePts + newPts)
			{
				delete desc.pc;
				desc.pc = nullptr;
			}
			else
			{
				return cmd.error(QObject::tr("Fusion failed! (not enough memory?)"));
			}
		}
	}

	// clean the 'cmd.clouds()' vector
	cmd.clouds().resize(1);
	// update the first one
	cmd.clouds().front().basename += QObject::tr("_MERGED");
	if (cmd.autoSaveMode())
	{
		QString errorStr = cmd.exportEntity(cmd.clouds().front());
		if (!errorStr.isEmpty())
		{
			return cmd.error(errorStr);
		}
	}
	return true;
}

CommandSetGlobalShift::CommandSetGlobalShift()
    : ccCommandLineInterface::Command(QObject::tr("Set global shift"), COMMAND_SET_GLOBAL_SHIFT)
{
}

bool CommandSetGlobalShift::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No loaded entity! (be sure to open one with \"-%1 [filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SET_GLOBAL_SHIFT));
	}

	// process globalshift options first
	ccCommandLineInterface::GlobalShiftOptions globalShiftOptions;
	cmd.processGlobalShiftCommand(globalShiftOptions);
	// if it is not a valid global shift then an error msg already issued.
	if (globalShiftOptions.mode != ccCommandLineInterface::GlobalShiftOptions::Mode::CUSTOM_GLOBAL_SHIFT)
	{
		return cmd.error(QObject::tr("Global shift must be in the form of three coordinates 'x' 'y' 'z'"));
	}
	CCVector3d newShift = globalShiftOptions.customGlobalShift;

	// look for additional parameters
	bool keepOrigFixed = false;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_SET_GLOBAL_SHIFT_KEEP_ORIG_FIXED))
		{
			// local option confirmed, pop that from front
			cmd.arguments().pop_front();

			keepOrigFixed = true;
			cmd.print(QObject::tr("[%1]").arg(COMMAND_SET_GLOBAL_SHIFT_KEEP_ORIG_FIXED));
		}
		else
		{
			break;
		}
	}

	// create an entity vector
	size_t                                                  nrOfClouds   = cmd.clouds().size();
	size_t                                                  nrOfMeshes   = cmd.meshes().size();
	size_t                                                  nrOfEntities = nrOfClouds + nrOfMeshes;
	std::vector<std::pair<ccShiftedObject*, CLEntityDesc*>> entities;
	entities.reserve(nrOfEntities);

	// add clouds to the vector
	for (CLCloudDesc& desc : cmd.clouds())
	{
		entities.push_back({desc.pc, &desc});
	}

	// add meshes to the vector
	for (CLMeshDesc& desc : cmd.meshes())
	{
		bool             isLocked = false;
		ccShiftedObject* shifted  = ccHObjectCaster::ToShifted(desc.mesh, &isLocked);
		if (shifted && !isLocked)
		{
			entities.push_back({shifted, &desc});
		}
	}

	// process both clouds and meshes
	for (const auto& pair : entities)
	{
		CLEntityDesc&    desc          = *pair.second;
		ccShiftedObject* shiftedObject = pair.first;
		CCVector3d       originalShift = shiftedObject->getGlobalShift();
		cmd.print(QObject::tr("\t[%4 - %5] Original global shift {%1,%2,%3}")
		              .arg(originalShift.x)
		              .arg(originalShift.y)
		              .arg(originalShift.z)
		              .arg(desc.basename)
		              .arg(shiftedObject->getName()));

		// translate entity to keep the initial global origin
		if (keepOrigFixed)
		{
			CCVector3d T = newShift - originalShift;
			ccGLMatrix transMat;
			double     maxCoordValue = ccGlobalShiftManager::MaxCoordinateAbsValue();
			if (T.x > maxCoordValue || T.y > maxCoordValue || T.z > maxCoordValue)
			{
				cmd.warning(QObject::tr("\t[%5 - %6] Applied transformation is bigger {%1,%2,%3} than the threshold {%4}, precision loss may occur.")
				                .arg(T.x)
				                .arg(T.y)
				                .arg(T.z)
				                .arg(maxCoordValue)
				                .arg(desc.basename)
				                .arg(shiftedObject->getName()));
			}

			cmd.print(QObject::tr("\t[%4 - %5] Applied Transformation {%1,%2,%3}")
			              .arg(T.x)
			              .arg(T.y)
			              .arg(T.z)
			              .arg(desc.basename)
			              .arg(shiftedObject->getName()));
			transMat.toIdentity();
			transMat.setTranslation(T);
			shiftedObject->applyGLTransformation_recursive(&transMat);
		}

		// apply new global shift
		shiftedObject->setGlobalShift(newShift.x, newShift.y, newShift.z);
		cmd.print(QObject::tr("\t[%4 - %5] Global shift set to {%1,%2,%3}")
		              .arg(newShift.x)
		              .arg(newShift.y)
		              .arg(newShift.z)
		              .arg(desc.basename)
		              .arg(shiftedObject->getName()));

		QString nameSuffix = QObject::tr("_SHIFTED_FROM_%1_%2_%3_TO_%4_%5_%6")
		                         .arg(originalShift.x)
		                         .arg(originalShift.y)
		                         .arg(originalShift.z)
		                         .arg(newShift.x)
		                         .arg(newShift.y)
		                         .arg(newShift.z);

		if ((&desc)->getCLEntityType() == CL_ENTITY_TYPE::MESH)
		{
			// set the mesh name instead of the vertices cloud inside the mesh
			ccHObject* parent = shiftedObject->getParent();
			if (parent)
			{
				parent->setName(QObject::tr("%1%2").arg(parent->getName()).arg(nameSuffix));
			}
		}
		else
		{
			shiftedObject->setName(QObject::tr("%1%2").arg(shiftedObject->getName()).arg(nameSuffix));
		}
		desc.basename += nameSuffix;

		// save it as well
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(desc);
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandSetActiveSF::CommandSetActiveSF()
    : ccCommandLineInterface::Command(QObject::tr("Set active SF"), COMMAND_SET_ACTIVE_SF)
{
}

bool CommandSetActiveSF::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: scalar field index after \"-%1\"").arg(COMMAND_SET_ACTIVE_SF));
	}

	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No point cloud nor mesh loaded! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_SET_ACTIVE_SF));
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFIndex = GetScalarFieldIndex(desc.pc, sfIndex, sfName, false);
			desc.pc->setCurrentScalarField(thisSFIndex);
		}
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		ccPointCloud* pc = ccHObjectCaster::ToPointCloud(desc.mesh);
		if (pc)
		{
			int thisSFIndex = GetScalarFieldIndex(pc, sfIndex, sfName, false);
			pc->setCurrentScalarField(thisSFIndex);
		}
	}

	return true;
}

CommandRemoveAllSFs::CommandRemoveAllSFs()
    : ccCommandLineInterface::Command(QObject::tr("Remove all SF"), COMMAND_REMOVE_ALL_SFS)
{
}

bool CommandRemoveAllSFs::process(ccCommandLineInterface& cmd)
{
	// no argument required
	for (auto& desc : cmd.clouds())
	{
		if (desc.pc /* && desc.pc->hasScalarFields()*/)
		{
			desc.pc->deleteAllScalarFields();
			desc.pc->showSF(false);
		}
	}

	for (auto& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			if (cloud->isA(CC_TYPES::POINT_CLOUD))
			{
				static_cast<ccPointCloud*>(cloud)->deleteAllScalarFields();
				cloud->showSF(false);
			}
		}
	}

	return true;
}

CommandRemoveSF::CommandRemoveSF()
    : ccCommandLineInterface::Command(QObject::tr("Remove a specific SF"), COMMAND_REMOVE_SF)
{
}

bool CommandRemoveSF::removeSF(int sfIndex, ccPointCloud& pc)
{
	if (sfIndex < static_cast<int>(pc.getNumberOfScalarFields()))
	{
		ccLog::Print("[REMOVE_SF] " + QString::fromStdString(pc.getScalarFieldName(sfIndex)));
		pc.deleteScalarField(sfIndex);
		if (pc.getNumberOfScalarFields() == 0)
		{
			pc.showSF(false);
		}
		else if (pc.getCurrentDisplayedScalarFieldIndex() < 0)
		{
			pc.setCurrentDisplayedScalarField(static_cast<int>(pc.getNumberOfScalarFields()) - 1);
		}

		return true;
	}
	else
	{
		return false;
	}
}

bool CommandRemoveSF::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: SF index after %1").arg(COMMAND_REMOVE_SF));
	}

	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	for (auto& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFIndex = GetScalarFieldIndex(desc.pc, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				removeSF(thisSFIndex, *desc.pc);
			}
		}
	}

	for (auto& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			if (cloud->isA(CC_TYPES::POINT_CLOUD))
			{
				ccPointCloud* pc = static_cast<ccPointCloud*>(cloud);

				int thisSFIndex = GetScalarFieldIndex(pc, sfIndex, sfName, true);
				if (thisSFIndex >= 0)
				{
					removeSF(thisSFIndex, *pc);
					if (pc->getNumberOfScalarFields() == 0)
					{
						desc.mesh->showSF(false);
					}
				}
			}
		}
	}

	return true;
}

CommandRemoveRGB::CommandRemoveRGB()
    : ccCommandLineInterface::Command(QObject::tr("Remove RGB"), COMMAND_REMOVE_RGB)
{
}

bool CommandRemoveRGB::process(ccCommandLineInterface& cmd)
{
	// no argument required
	for (auto& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			desc.pc->unallocateColors();
			desc.pc->showColors(false);
		}
	}

	for (auto& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			if (cloud->isA(CC_TYPES::POINT_CLOUD))
			{
				static_cast<ccPointCloud*>(cloud)->unallocateColors();
				cloud->showColors(false);
			}
			desc.mesh->showColors(false);
		}
	}

	return true;
}

CommandRemoveNormals::CommandRemoveNormals()
    : ccCommandLineInterface::Command(QObject::tr("Remove normals"), COMMAND_REMOVE_NORMALS)
{
}

bool CommandRemoveNormals::process(ccCommandLineInterface& cmd)
{
	// no argument required
	for (auto& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			desc.pc->unallocateNorms();
			desc.pc->showNormals(false);
		}
	}

	for (auto& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			if (cloud->isA(CC_TYPES::POINT_CLOUD))
			{
				static_cast<ccPointCloud*>(cloud)->unallocateNorms();
				cloud->showNormals(false);
			}
			if (desc.mesh->isA(CC_TYPES::MESH))
			{
				static_cast<ccMesh*>(desc.mesh)->clearTriNormals();
				desc.mesh->showNormals(false);
			}
		}
	}

	return true;
}

CommandRemoveScanGrids::CommandRemoveScanGrids()
    : ccCommandLineInterface::Command(QObject::tr("Remove scan grids"), COMMAND_REMOVE_SCAN_GRIDS)
{
}

bool CommandRemoveScanGrids::process(ccCommandLineInterface& cmd)
{
	// no argument required
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			desc.pc->removeGrids();
		}
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			if (cloud->isA(CC_TYPES::POINT_CLOUD))
			{
				static_cast<ccPointCloud*>(cloud)->removeGrids();
			}
		}
	}

	return true;
}

CommandRemoveSensors::CommandRemoveSensors()
    : ccCommandLineInterface::Command(QObject::tr("Remove sensors"), COMMAND_REMOVE_SENSORS)
{
}

static void RemoveSensors(ccHObject* entity)
{
	if (!entity)
	{
		assert(false);
		return;
	}

	ccHObject::Container sensors;
	entity->filterChildren(sensors, false, CC_TYPES::SENSOR, false); // direct children only

	for (ccHObject* sensor : sensors)
	{
		ccLog::Print(QString("Removing sensor '%1' from entity '%2'").arg(sensor->getName()).arg(entity->getName()));
		entity->removeChild(sensor);
	}
}

bool CommandRemoveSensors::process(ccCommandLineInterface& cmd)
{
	// no argument required
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			RemoveSensors(desc.pc);
		}
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			RemoveSensors(desc.mesh);

			ccGenericPointCloud* cloud = desc.mesh->getAssociatedCloud();
			RemoveSensors(cloud);
		}
	}

	return true;
}

CommandMatchBBCenters::CommandMatchBBCenters()
    : ccCommandLineInterface::Command(QObject::tr("Match B.B. centers"), COMMAND_MATCH_BB_CENTERS)
{
}

bool CommandMatchBBCenters::process(ccCommandLineInterface& cmd)
{
	std::vector<CLEntityDesc*> entities;
	for (auto& cloud : cmd.clouds())
	{
		entities.push_back(&cloud);
	}
	for (auto& mesh : cmd.meshes())
	{
		entities.push_back(&mesh);
	}

	if (entities.empty())
	{
		return cmd.error("No entity loaded!");
	}
	else if (entities.size() == 1)
	{
		cmd.warning("Nothing to do: only one entity currently loaded!");
		return true;
	}

	CCVector3 firstCenter = entities.front()->getEntity()->getOwnBB().getCenter();
	for (size_t i = 1; i < entities.size(); ++i)
	{
		ccHObject* ent    = entities[i]->getEntity();
		CCVector3  center = ent->getOwnBB().getCenter();
		CCVector3  T      = firstCenter - center;

		// transformation (used only for translation)
		ccGLMatrix glTrans;
		glTrans += T;

		// apply translation matrix
		ent->applyGLTransformation_recursive(&glTrans);
		cmd.print(QObject::tr("Entity '%1' has been translated: (%2,%3,%4)").arg(ent->getName()).arg(T.x).arg(T.y).arg(T.z));
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(*entities[i]);
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	return true;
}

CommandMatchBestFitPlane::CommandMatchBestFitPlane()
    : ccCommandLineInterface::Command(QObject::tr("Compute best fit plane"), COMMAND_BEST_FIT_PLANE)
{
}

bool CommandMatchBestFitPlane::process(ccCommandLineInterface& cmd)
{
	// look for local options
	bool makeCloudsHoriz = false;
	bool keepLoaded      = false;

	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_BEST_FIT_PLANE_MAKE_HORIZ))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			makeCloudsHoriz = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_BEST_FIT_PLANE_KEEP_LOADED))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			keepLoaded = true;
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No cloud available. Be sure to open one first!"));
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		// try to fit plane
		double   rms    = 0.0;
		ccPlane* pPlane = ccPlane::Fit(desc.pc, &rms);
		if (pPlane)
		{
			cmd.print(QObject::tr("Plane successfully fitted: rms = %1").arg(rms));

			CCVector3 N = pPlane->getNormal();
			CCVector3 C = *CCCoreLib::Neighbourhood(desc.pc).getGravityCenter();

			CLMeshDesc planeDesc;
			planeDesc.mesh     = pPlane;
			planeDesc.basename = desc.basename;
			planeDesc.path     = desc.path;

			// save plane as a BIN file
			QString outputFilename;
			QString errorStr = cmd.exportEntity(planeDesc, "BEST_FIT_PLANE", &outputFilename);
			if (!errorStr.isEmpty())
			{
				cmd.warning(errorStr);
			}

			// compute the transformation matrix that would make this normal points towards +Z
			ccGLMatrix makeZPosMatrix = ccGLMatrix::FromToRotation(N, CCVector3(0, 0, CCCoreLib::PC_ONE));
			CCVector3  Gt             = C;
			makeZPosMatrix.applyRotation(Gt);
			makeZPosMatrix.setTranslation(C - Gt);

			// open text file to save plane related information
			{
				QString txtFilename = QObject::tr("%1/%2_BEST_FIT_PLANE_INFO").arg(desc.path, desc.basename);
				if (cmd.addTimestamp())
				{
					QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_hh'h'mm_ss_zzz");
					txtFilename += QObject::tr("_%1").arg(timestamp);
				}
				txtFilename += QObject::tr(".txt");
				QFile txtFile(txtFilename);
				if (txtFile.open(QIODevice::WriteOnly | QIODevice::Text))
				{
					QTextStream txtStream(&txtFile);

					txtStream << QObject::tr("Filename: %1").arg(outputFilename) << endl;
					txtStream << QObject::tr("Fitting RMS: %1").arg(rms) << endl;

					// We always consider the normal with a positive 'Z' by default!
					if (N.z < 0.0)
					{
						N *= -1.0;
					}

					int precision = cmd.numericalPrecision();
					txtStream << QObject::tr("Normal: (%1,%2,%3)").arg(N.x, 0, 'f', precision).arg(N.y, 0, 'f', precision).arg(N.z, 0, 'f', precision) << endl;

					// we compute strike & dip by the way
					{
						PointCoordinateType dip    = 0;
						PointCoordinateType dipDir = 0;
						ccNormalVectors::ConvertNormalToDipAndDipDir(N, dip, dipDir);
						txtStream << ccNormalVectors::ConvertDipAndDipDirToString(dip, dipDir) << endl;
					}

					txtStream << "Orientation matrix:" << endl;
					txtStream << makeZPosMatrix.toString(precision, ' ') << endl;

					// close the text file
					txtFile.close();
				}
				else
				{
					cmd.warning("Failed to open file " + txtFilename + " for writing");
				}
			}

			if (keepLoaded)
			{
				// add the resulting plane (mesh) to the main set
				cmd.meshes().push_back(planeDesc);
			}

			if (makeCloudsHoriz)
			{
				// apply 'horizontal' matrix
				desc.pc->applyGLTransformation_recursive(&makeZPosMatrix);
				cmd.print(QObject::tr("Cloud '%1' has been transformed with the above matrix").arg(desc.pc->getName()));
				desc.basename += QObject::tr("_HORIZ");
				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc);
					if (!errorStr.isEmpty())
					{
						cmd.warning(errorStr);
					}
				}
			}
		}
		else
		{
			cmd.warning(QObject::tr("Failed to compute best fit plane for cloud '%1'").arg(desc.pc->getName()));
		}
	}

	return true;
}

CommandOrientNormalsMST::CommandOrientNormalsMST()
    : ccCommandLineInterface::Command(QObject::tr("Orient normals"), COMMAND_ORIENT_NORMALS)
{
}

bool CommandOrientNormalsMST::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: number of neighbors after \"-%1\"").arg(COMMAND_ORIENT_NORMALS));
	}

	QString knnStr = cmd.arguments().takeFirst();
	bool    ok;
	int     knn = knnStr.toInt(&ok);
	if (!ok || knn <= 0)
	{
		return cmd.error(QObject::tr("Invalid parameter: number of neighbors (%1)").arg(knnStr));
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No cloud available. Be sure to open one first!"));
	}

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		assert(desc.pc);

		if (!desc.pc->hasNormals())
		{
			continue;
		}

		// computation
		if (desc.pc->orientNormalsWithMST(knn, progressDialog.data()))
		{
			desc.basename += QObject::tr("_NORMS_REORIENTED");
			if (cmd.autoSaveMode())
			{
				QString errorStr = cmd.exportEntity(desc);
				if (!errorStr.isEmpty())
				{
					cmd.warning(errorStr);
				}
			}
		}
		else
		{
			return cmd.error(QObject::tr("Failed to orient the normals of cloud '%1'!").arg(desc.pc->getName()));
		}
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandSORFilter::CommandSORFilter()
    : ccCommandLineInterface::Command(QObject::tr("S.O.R. filter"), COMMAND_SOR_FILTER)
{
}

bool CommandSORFilter::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: number of neighbors mode after \"-%1\"").arg(COMMAND_SOR_FILTER));
	}

	QString knnStr = cmd.arguments().takeFirst();
	bool    ok;
	int     knn = knnStr.toInt(&ok);
	if (!ok || knn <= 0)
	{
		return cmd.error(QObject::tr("Invalid parameter: number of neighbors (%1)").arg(knnStr));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: sigma multiplier after number of neighbors (SOR)"));
	}
	QString sigmaStr = cmd.arguments().takeFirst();
	double  nSigma   = sigmaStr.toDouble(&ok);
	if (!ok || nSigma < 0)
	{
		return cmd.error(QObject::tr("Invalid parameter: sigma multiplier (%1)").arg(nSigma));
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No cloud available. Be sure to open one first!"));
	}

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		assert(desc.pc);

		// computation
		CCCoreLib::ReferenceCloud* selection = CCCoreLib::CloudSamplingTools::sorFilter(desc.pc,
		                                                                                knn,
		                                                                                nSigma,
		                                                                                nullptr,
		                                                                                progressDialog.data());

		if (selection)
		{
			ccPointCloud* cleanCloud = desc.pc->partialClone(selection);
			if (cleanCloud)
			{
				cleanCloud->setName(desc.pc->getName() + QObject::tr(".clean"));
				if (cmd.autoSaveMode())
				{
					CLCloudDesc newDesc(cleanCloud, desc.basename, desc.path, desc.indexInFile);
					QString     errorStr = cmd.exportEntity(newDesc, "SOR");
					if (!errorStr.isEmpty())
					{
						delete cleanCloud;
						return cmd.error(errorStr);
					}
				}
				// replace current cloud by this one
				delete desc.pc;
				desc.pc = cleanCloud;
				desc.basename += QObject::tr("_SOR");
				// delete cleanCloud;
				// cleanCloud = 0;
			}
			else
			{
				return cmd.error(QObject::tr("Not enough memory to create a clean version of cloud '%1'!").arg(desc.pc->getName()));
			}

			delete selection;
			selection = nullptr;
		}
		else
		{
			// no points fall inside selection!
			return cmd.error(QObject::tr("Failed to apply SOR filter on cloud '%1'! (empty output or not enough memory?)").arg(desc.pc->getName()));
		}
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandNoiseFilter::CommandNoiseFilter()
    : ccCommandLineInterface::Command(QObject::tr("Noise filter"), COMMAND_NOISE_FILTER)
{
}

bool CommandNoiseFilter::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 4)
	{
		return cmd.error(QObject::tr("Missing parameters: 'KNN/RADIUS {value} REL/ABS {value}' expected after \"-%1\"").arg(COMMAND_NOISE_FILTER));
	}

	QString firstOption = cmd.arguments().takeFirst().toUpper();
	int     knn         = -1;
	double  radius      = std::numeric_limits<double>::quiet_NaN();

	if (firstOption == COMMAND_NOISE_FILTER_KNN)
	{
		QString knnStr = cmd.arguments().takeFirst();
		bool    ok;
		knn = knnStr.toInt(&ok);
		if (!ok || knn <= 0)
		{
			return cmd.error(QObject::tr("Invalid parameter: number of neighbors after KNN (got '%1' instead)").arg(knnStr));
		}
	}
	else if (firstOption == COMMAND_NOISE_FILTER_RADIUS)
	{
		QString radiusStr = cmd.arguments().takeFirst();
		bool    ok;
		radius = radiusStr.toDouble(&ok);
		if (!ok || radius <= 0)
		{
			return cmd.error(QObject::tr("Invalid parameter: radius after RADIUS (got '%1' instead)").arg(radiusStr));
		}
	}
	else
	{
		return cmd.error(QObject::tr("Invalid parameter: KNN or RADIUS expected after \"-%1\"").arg(COMMAND_NOISE_FILTER));
	}

	QString secondOption  = cmd.arguments().takeFirst().toUpper();
	bool    absoluteError = true;
	if (secondOption == COMMAND_NOISE_FILTER_REL)
	{
		absoluteError = false;
	}
	else if (secondOption == COMMAND_NOISE_FILTER_ABS)
	{
		absoluteError = true;
	}
	else
	{
		return cmd.error(QObject::tr("Invalid parameter: REL or ABS expected"));
	}

	double error = std::numeric_limits<double>::quiet_NaN();
	{
		QString errorStr = cmd.arguments().takeFirst();
		bool    ok;
		error = errorStr.toDouble(&ok);
		if (!ok || error <= 0)
		{
			return cmd.error(QObject::tr("Invalid parameter: relative or absolute error expected after KNN (got '%1' instead)").arg(errorStr));
		}
	}

	// optional arguments
	bool removeIsolatedPoints = false;
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (argument == COMMAND_NOISE_FILTER_RIP)
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			removeIsolatedPoints = true;
		}
	}

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		assert(desc.pc);

		// computation
		CCCoreLib::ReferenceCloud* selection = CCCoreLib::CloudSamplingTools::noiseFilter(desc.pc,
		                                                                                  static_cast<PointCoordinateType>(radius),
		                                                                                  error,
		                                                                                  removeIsolatedPoints,
		                                                                                  knn > 0,
		                                                                                  knn,
		                                                                                  absoluteError,
		                                                                                  error,
		                                                                                  nullptr,
		                                                                                  progressDialog.data());

		if (selection)
		{
			ccPointCloud* cleanCloud = desc.pc->partialClone(selection);
			if (cleanCloud)
			{
				cleanCloud->setName(desc.pc->getName() + QObject::tr(".clean"));
				if (cmd.autoSaveMode())
				{
					CLCloudDesc newDesc(cleanCloud, desc.basename, desc.path, desc.indexInFile);
					QString     errorStr = cmd.exportEntity(newDesc, "DENOISED");
					if (!errorStr.isEmpty())
					{
						delete cleanCloud;
						return cmd.error(errorStr);
					}
				}
				// replace current cloud by this one
				delete desc.pc;
				desc.pc = cleanCloud;
				desc.basename += QObject::tr("_DENOISED");
				// delete cleanCloud;
				// cleanCloud = 0;
			}
			else
			{
				return cmd.error(QObject::tr("Not enough memory to create a clean version of cloud '%1'!").arg(desc.pc->getName()));
			}

			delete selection;
			selection = nullptr;
		}
		else
		{
			// no points fall inside selection!
			return cmd.error(QObject::tr("Failed to apply Noise filter on cloud '%1'! (empty output or not enough memory?)").arg(desc.pc->getName()));
		}
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandRemoveDuplicatePoints::CommandRemoveDuplicatePoints()
    : ccCommandLineInterface::Command(QObject::tr("Remove duplicate points"), COMMAND_REMOVE_DUPLICATE_POINTS)
{
}

bool CommandRemoveDuplicatePoints::process(ccCommandLineInterface& cmd)
{
	double minDistanceBetweenPoints = std::numeric_limits<double>::epsilon();

	// get optional argument
	if (!cmd.arguments().empty())
	{
		bool   paramOk = false;
		double arg     = cmd.arguments().front().toDouble(&paramOk);
		if (paramOk)
		{
			if (arg < minDistanceBetweenPoints)
			{
				return cmd.error(QObject::tr("Invalid argument: '%1'").arg(arg));
			}

			minDistanceBetweenPoints = arg;
			cmd.arguments().pop_front();
		}
	}

	cmd.print(QObject::tr("Minimum distance between points: '%1'").arg(minDistanceBetweenPoints));

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		assert(desc.pc);
		ccPointCloud* filteredCloud = desc.pc->removeDuplicatePoints(minDistanceBetweenPoints, progressDialog.data());
		if (!filteredCloud)
		{
			return cmd.error(QObject::tr("Process failed (see log)"));
		}
		if (filteredCloud == desc.pc)
		{
			// nothing to do
			continue;
		}

		// replace current cloud by filtered one
		delete desc.pc;
		desc.pc = filteredCloud;
		desc.basename += QObject::tr("_REMOVED_DUPLICATE_POINTS");
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandExtractVertices::CommandExtractVertices()
    : ccCommandLineInterface::Command(QObject::tr("Extract vertices"), COMMAND_EXTRACT_VERTICES)
{
}

bool CommandExtractVertices::process(ccCommandLineInterface& cmd)
{
	if (cmd.meshes().empty())
	{
		cmd.warning(QObject::tr("No mesh available. Be sure to open one first!"));
		return false;
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		ccGenericMesh*       mesh  = desc.mesh;
		ccGenericPointCloud* cloud = mesh->getAssociatedCloud();
		ccPointCloud*        pc    = ccHObjectCaster::ToPointCloud(cloud);
		if (!pc)
		{
			assert(false);
			continue;
		}

		// add the resulting cloud to the main set
		cmd.clouds().emplace_back(pc, desc.basename + QObject::tr(".vertices"), desc.path);

		// don't forget to detach the cloud before we delete the meshes!
		assert(pc->getParent() == mesh);
		if (pc->getParent())
		{
			pc->getParent()->detachChild(pc);
		}

		// save it as well
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(cmd.clouds().back());
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	cmd.removeMeshes(false);

	return true;
}

CommandFlipTriangles::CommandFlipTriangles()
    : ccCommandLineInterface::Command(QObject::tr("Flip mesh triangles"), COMMAND_FLIP_TRIANGLES)
{
}

bool CommandFlipTriangles::process(ccCommandLineInterface& cmd)
{
	if (cmd.meshes().empty())
	{
		cmd.warning(QObject::tr("No mesh available. Be sure to open one first!"));
		return false;
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		ccGenericMesh* mesh = desc.mesh;

		ccMesh* ccMesh = ccHObjectCaster::ToMesh(mesh);
		if (ccMesh)
		{
			ccMesh->flipTriangles();
		}

		desc.basename += QObject::tr("_FLIPPED_TRIANGLES");

		// save it as well
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(desc);
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	cmd.removeMeshes(false);

	return true;
}

CommandSampleMesh::CommandSampleMesh()
    : ccCommandLineInterface::Command(QObject::tr("Sample mesh"), COMMAND_SAMPLE_MESH)
{
}

bool CommandSampleMesh::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: sampling mode after \"-%1\" (POINTS/DENSITY)").arg(COMMAND_SAMPLE_MESH));
	}

	bool   useDensity = false;
	double parameter  = 0;

	QString sampleMode = cmd.arguments().takeFirst().toUpper();
	if (sampleMode == "POINTS")
	{
		useDensity = false;
	}
	else if (sampleMode == "DENSITY")
	{
		useDensity = true;
	}
	else
	{
		return cmd.error(QObject::tr("Invalid parameter: unknown sampling mode \"%1\"").arg(sampleMode));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: value after sampling mode"));
	}
	bool conversionOk = false;
	parameter         = cmd.arguments().takeFirst().toDouble(&conversionOk);
	if (!conversionOk)
	{
		return cmd.error(QObject::tr("Invalid parameter: value after sampling mode"));
	}

	if (cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No mesh available. Be sure to open one first!"));
	}

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLMeshDesc& desc : cmd.meshes())
	{
		ccPointCloud* cloud = desc.mesh->samplePoints(useDensity, parameter, true, true, true, progressDialog.data());

		if (!cloud)
		{
			return cmd.error(QObject::tr("Cloud sampling failed!"));
		}

		// add the resulting cloud to the main set
		cmd.print(QObject::tr("Sampled cloud created: %1 points").arg(cloud->size()));
		cmd.clouds().emplace_back(cloud, desc.basename + QObject::tr("_SAMPLED_POINTS"), desc.path);

		// save it as well
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(cmd.clouds().back());
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandCompressFWF::CommandCompressFWF()
    : ccCommandLineInterface::Command(QObject::tr("Compress FWF"), COMMAND_COMPRESS_FWF)
{
}

bool CommandCompressFWF::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud available. Be sure to open or generate one first!"));
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		desc.pc->compressFWFData();
	}

	return true;
}

CommandCrop::CommandCrop()
    : ccCommandLineInterface::Command(QObject::tr("Crop"), COMMAND_CROP)
{
}

bool CommandCrop::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: box extents after \"-%1\" (Xmin:Ymin:Zmin:Xmax:Ymax:Zmax)").arg(COMMAND_CROP));
	}
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No point cloud or mesh available. Be sure to open or generate one first!"));
	}

	// decode box extents
	CCVector3 boxMin;
	CCVector3 boxMax;
	{
		QString     boxBlock = cmd.arguments().takeFirst();
		QStringList tokens   = boxBlock.split(':');
		if (tokens.size() != 6)
		{
			return cmd.error(QObject::tr("Invalid parameter: box extents (expected format is 'Xmin:Ymin:Zmin:Xmax:Ymax:Zmax')").arg(COMMAND_CROP));
		}

		for (int i = 0; i < 6; ++i)
		{
			CCVector3* vec = (i < 3 ? &boxMin : &boxMax);
			bool       ok  = true;
			vec->u[i % 3]  = static_cast<PointCoordinateType>(tokens[i].toDouble(&ok));
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid parameter: box extents (component #%1 is not a valid number)").arg(i + 1));
			}
		}
	}

	// optional parameters
	bool inside = true;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_CROP_OUTSIDE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			inside = false;
		}
		else
		{
			break;
		}
	}

	ccBBox cropBox(boxMin, boxMax, true);
	// crop clouds
	{
		for (CLCloudDesc& desc : cmd.clouds())
		{
			ccHObject* croppedCloud = ccCropTool::Crop(desc.pc, cropBox, inside);
			if (croppedCloud)
			{
				delete desc.pc;
				assert(croppedCloud->isA(CC_TYPES::POINT_CLOUD));
				desc.pc = static_cast<ccPointCloud*>(croppedCloud);
				desc.basename += "_CROPPED";
				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc);
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
			else
			{
				// otherwise an error message has already been issued
				delete desc.pc;
				desc.pc = nullptr; // will be removed after this loop
				                   // desc.basename += "_FULLY_CROPPED";
			}
		}

		// now clean the set of clouds in case some have been 'cropped out'
		for (auto it = cmd.clouds().begin(); it != cmd.clouds().end();)
		{
			if (it->pc == nullptr)
			{
				it = cmd.clouds().erase(it);
			}
			else
			{
				++it;
			}
		}
	}

	// crop meshes
	{
		for (CLMeshDesc& desc : cmd.meshes())
		{
			ccHObject* croppedMesh = ccCropTool::Crop(desc.mesh, cropBox, inside);
			if (croppedMesh)
			{
				delete desc.mesh;
				assert(croppedMesh->isA(CC_TYPES::MESH));
				desc.mesh = static_cast<ccMesh*>(croppedMesh);
				desc.basename += "_CROPPED";
				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc);
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
			else
			{
				// otherwise an error message has already been issued
				delete desc.mesh;
				desc.mesh = nullptr; // will be removed after this loop
				                     // desc.basename += "_FULLY_CROPPED";
			}
		}

		// now clean the set of meshes in case some have been 'cropped out'
		for (auto it = cmd.meshes().begin(); it != cmd.meshes().end();)
		{
			if (it->mesh == nullptr)
			{
				it = cmd.meshes().erase(it);
			}
			else
			{
				++it;
			}
		}
	}

	return true;
}

CommandSFToCoord::CommandSFToCoord()
    : ccCommandLineInterface::Command(QObject::tr("SF to Coord"), COMMAND_SF_TO_COORD)
{
}

bool CommandSFToCoord::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 2)
	{
		return cmd.error(QObject::tr("Missing parameter(s) after \"-%1\" (SF INDEX OR NAME) (DIMENSION)").arg(COMMAND_SF_TO_COORD));
	}
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud available. Be sure to open or generate one first!"));
	}

	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName))
	{
		return false;
	}

	// dimension
	QString dimStr = cmd.arguments().takeFirst().toUpper();
	bool    exportDims[3]{dimStr == "X", dimStr == "Y", dimStr == "Z"};
	if (!exportDims[0] && !exportDims[1] && !exportDims[2])
	{
		return cmd.error(QObject::tr("Invalid parameter: dimension after \"-%1\" (expected: X, Y or Z)").arg(COMMAND_SF_TO_COORD));
	}

	// now we can export the corresponding coordinate
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			CCCoreLib::ScalarField* sf = GetScalarField(desc.pc, sfIndex, sfName, true);
			if (sf)
			{
				if (desc.pc->setCoordFromSF(exportDims, sf, std::numeric_limits<PointCoordinateType>::quiet_NaN()))
				{
					desc.basename += QObject::tr("_SF_TO_COORD_%1").arg(dimStr);
					if (cmd.autoSaveMode())
					{
						QString errorStr = cmd.exportEntity(desc);
						if (!errorStr.isEmpty())
						{
							return cmd.error(errorStr);
						}
					}
				}
			}
			else
			{
				return cmd.error(QObject::tr("Failed to set SF %1 as coord %2 on cloud '%3'!").arg(sfName).arg(dimStr).arg(desc.pc->getName()));
			}
		}
	}

	return true;
}

CommandCoordToSF::CommandCoordToSF()
    : ccCommandLineInterface::Command(QObject::tr("Coord to SF"), COMMAND_COORD_TO_SF)
{
}

bool CommandCoordToSF::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter after \"-%1\" (DIMENSION)").arg(COMMAND_COORD_TO_SF));
	}
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud available. Be sure to open or generate one first!"));
	}

	// dimension
	QString dimStr = cmd.arguments().takeFirst().toUpper();
	bool    exportDims[3]{dimStr == "X", dimStr == "Y", dimStr == "Z"};
	if (!exportDims[0] && !exportDims[1] && !exportDims[2])
	{
		return cmd.error(QObject::tr("Invalid parameter: dimension after \"-%1\" (expected: X, Y or Z)").arg(COMMAND_COORD_TO_SF));
	}

	// now we can export the corresponding coordinate
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc->exportCoordToSF(exportDims))
		{
			desc.basename += QObject::tr("_%1_TO_SF").arg(dimStr);
			if (cmd.autoSaveMode())
			{
				QString errorStr = cmd.exportEntity(desc);
				if (!errorStr.isEmpty())
				{
					return cmd.error(errorStr);
				}
			}
		}
		else
		{
			return cmd.error(QObject::tr("Failed to export coord. %1 to SF on cloud '%2'!").arg(dimStr, desc.pc->getName()));
		}
	}

	return true;
}

CommandCrop2D::CommandCrop2D()
    : ccCommandLineInterface::Command(QObject::tr("Crop 2D"), COMMAND_CROP_2D)
{
}

bool CommandCrop2D::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 6)
	{
		return cmd.error(QObject::tr("Missing parameter(s) after \"-%1\" (ORTHO_DIM N X1 Y1 X2 Y2 ... XN YN)").arg(COMMAND_CROP_2D));
	}
	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud available. Be sure to open or generate one first!"));
	}

	// decode poyline extents
	ccPointCloud vertices("polyline.vertices");
	ccPolyline   poly(&vertices);

	// orthogonal dimension
	unsigned char orthoDim     = 2;
	bool          orderFlipped = false;
	{
		QString orthoDimStr = cmd.arguments().takeFirst().toUpper();
		if (orthoDimStr.endsWith("FLIP"))
		{
			orderFlipped = true;
			orthoDimStr  = orthoDimStr.left(orthoDimStr.size() - 4);
		}

		if (orthoDimStr == "X")
		{
			orthoDim = 0;
		}
		else if (orthoDimStr == "Y")
		{
			orthoDim = 1;
		}
		else if (orthoDimStr == "Z")
		{
			orthoDim = 2;
		}
		else
		{
			return cmd.error(QObject::tr("Invalid parameter: orthogonal dimension after \"-%1\" (expected: X, Y or Z)").arg(COMMAND_CROP_2D));
		}
	}

	ccCommandLineInterface::GlobalShiftOptions globalShiftOptions;
	globalShiftOptions.mode = ccCommandLineInterface::GlobalShiftOptions::NO_GLOBAL_SHIFT;

	if (cmd.arguments().size() >= 4)
	{
		if (cmd.nextCommandIsGlobalShift())
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (!cmd.processGlobalShiftCommand(globalShiftOptions))
			{
				// error message already issued
				return false;
			}

			cmd.setGlobalShiftOptions(globalShiftOptions);
		}
	}

	// number of vertices
	bool     ok = true;
	unsigned N  = 0;
	{
		QString countStr = cmd.arguments().takeFirst();
		N                = countStr.toUInt(&ok);
		if (!ok)
		{
			return cmd.error(QObject::tr("Invalid parameter: number of vertices for the 2D polyline after \"-%1\"").arg(COMMAND_CROP_2D));
		}
	}

	// now read the vertices
	{
		if (!vertices.reserve(N) || !poly.addPointIndex(0, N))
		{
			return cmd.error(QObject::tr("Not enough memory!"));
		}

		assert(orthoDim < 3);
		unsigned char X = ((orthoDim + 1) % 3);
		unsigned char Y = ((X + 1) % 3);

		unsigned char Xread = X;
		unsigned char Yread = Y;
		if (orderFlipped)
		{
			std::swap(Xread, Yread);
		}

		CCVector3d PShift(0, 0, 0);
		for (unsigned i = 0; i < N; ++i)
		{
			if (cmd.arguments().size() < 2)
			{
				return cmd.error(QObject::tr("Missing parameter(s): vertex #%1 data and following").arg(i + 1));
			}

			CCVector3d Pd(0, 0, 0);

			QString coordStr = cmd.arguments().takeFirst();
			Pd.u[Xread]      = coordStr.toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid parameter: X-coordinate of vertex #%1").arg(i + 1));
			}
			/*QString */ coordStr = cmd.arguments().takeFirst();
			Pd.u[Yread]           = coordStr.toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid parameter: Y-coordinate of vertex #%1").arg(i + 1));
			}

			if (i == 0)
			{
				bool preserveCoordinateShift = false; // ignored
				if (FileIOFilter::HandleGlobalShift(Pd, PShift, preserveCoordinateShift, cmd.fileLoadingParams()))
				{
					ccLog::Warning(QString("[%1] 2D polyline has been recentered! Translation: (%2 ; %3 ; %4)").arg(COMMAND_CROP_2D).arg(PShift.x, 0, 'f', 2).arg(PShift.y, 0, 'f', 2).arg(PShift.z, 0, 'f', 2));
				}
			}

			CCVector3 P3D = (Pd + PShift).toPC();

			// warning: the polyline must be defined in the XY plane!
			CCVector3 P2D;
			{
				P2D.x = P3D.u[X];
				P2D.y = P3D.u[Y];
				P2D.z = 0;
			}
			vertices.addPoint(P2D);
		}

		poly.setClosed(true);
	}

	cmd.updateInteralGlobalShift(globalShiftOptions);

	// optional parameters
	bool inside = true;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_CROP_OUTSIDE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			inside = false;
		}
		else
		{
			break;
		}
	}

	// now we can crop the loaded cloud(s)
	for (CLCloudDesc& desc : cmd.clouds())
	{
		CCCoreLib::ReferenceCloud* ref = desc.pc->crop2D(&poly, orthoDim, inside);
		if (ref)
		{
			if (ref->size() != 0)
			{
				ccPointCloud* croppedCloud = desc.pc->partialClone(ref);
				delete ref;
				ref = nullptr;

				if (croppedCloud)
				{
					delete desc.pc;
					desc.pc = croppedCloud;
					croppedCloud->setName(desc.pc->getName() + QObject::tr(".cropped"));
					desc.basename += "_CROPPED";
					if (cmd.autoSaveMode())
					{
						QString errorStr = cmd.exportEntity(desc);
						if (!errorStr.isEmpty())
						{
							return cmd.error(errorStr);
						}
					}
				}
				else
				{
					return cmd.error(QObject::tr("Not enough memory to crop cloud '%1'!").arg(desc.pc->getName()));
				}
			}
			else
			{
				delete ref;
				ref = nullptr;
				cmd.warning(QObject::tr("No point of cloud '%1' falls inside the input box!").arg(desc.pc->getName()));
			}
		}
		else
		{
			return cmd.error(QObject::tr("Crop process failed! (not enough memory)"));
		}
	}

	return true;
}

CommandColorBanding::CommandColorBanding()
    : ccCommandLineInterface::Command(QObject::tr("Color banding"), COMMAND_COLOR_BANDING)
{
}

bool CommandColorBanding::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 2)
	{
		return cmd.error(QObject::tr("Missing parameter(s) after \"-%1\" (DIM FREQUENCY)").arg(COMMAND_COLOR_BANDING));
	}
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No entity available. Be sure to open or generate one first!"));
	}

	// dimension
	unsigned char dim    = 2;
	QString       dimStr = "Z";
	{
		dimStr = cmd.arguments().takeFirst().toUpper();
		if (dimStr == "X")
		{
			dim = 0;
		}
		else if (dimStr == "Y")
		{
			dim = 1;
		}
		else if (dimStr == "Z")
		{
			dim = 2;
		}
		else
		{
			return cmd.error(QObject::tr("Invalid parameter: dimension after \"-%1\" (expected: X, Y or Z)").arg(COMMAND_COLOR_BANDING));
		}
	}

	// frequency
	bool   ok   = true;
	double freq = 0;
	{
		QString countStr = cmd.arguments().takeFirst();
		freq             = countStr.toDouble(&ok);
		if (!ok)
		{
			return cmd.error(QObject::tr("Invalid parameter: frequency after \"-%1 DIM\" (in Hz, integer value)").arg(COMMAND_COLOR_BANDING));
		}
	}

	// process clouds
	if (!cmd.clouds().empty())
	{
		bool hasclouds = false;
		for (CLCloudDesc& desc : cmd.clouds())
		{
			if (desc.pc)
			{
				if (!desc.pc->setRGBColorByBanding(dim, freq))
				{
					return cmd.error(QObject::tr("Not enough memory"));
				}
				else
				{
					hasclouds = true;
				}
			}
		}

		// save output
		if (hasclouds && cmd.autoSaveMode() && !cmd.saveClouds(QObject::tr("COLOR_BANDING_%1_%2").arg(dimStr).arg(freq)))
		{
			return false;
		}
	}

	if (!cmd.meshes().empty())
	{
		bool hasMeshes = false;
		for (CLMeshDesc& desc : cmd.meshes())
		{
			ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(desc.mesh);
			if (cloud)
			{
				if (!cloud->setRGBColorByBanding(dim, freq))
				{
					return cmd.error(QObject::tr("Not enough memory"));
				}
				else
				{
					desc.mesh->showColors(true);
					hasMeshes = true;
				}
			}
			else
			{
				cmd.warning(QObject::tr("Vertices of mesh '%1' are locked (they may be shared by multiple entities for instance). Can't apply the current command on them.").arg(desc.mesh->getName()));
			}
		}

		// save output
		if (hasMeshes && cmd.autoSaveMode() && !cmd.saveMeshes(QObject::tr("COLOR_BANDING_%1_%2").arg(dimStr).arg(freq)))
		{
			return false;
		}
	}

	return true;
}

CommandColorLevels::CommandColorLevels()
    : ccCommandLineInterface::Command(QObject::tr("Color levels"), COMMAND_COLOR_LEVELS)
{
}

bool CommandColorLevels::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 5)
	{
		return cmd.error(QObject::tr("Missing parameter(s) after \"-%1\" (COLOR-BANDS MIN-INPUT-LEVEL MAX-INPUT-LEVEL MIN-OUTPUT-LEVEL MAX-OUTPUT-LEVEL)").arg(COMMAND_COLOR_LEVELS));
	}
	if (cmd.clouds().empty() && cmd.meshes().empty())
	{
		return cmd.error(QObject::tr("No entity available. Be sure to open or generate one first!"));
	}

	// color bands
	QString band = cmd.arguments().takeFirst().toUpper();
	bool    rgb[3]{band.contains('R'), band.contains('G'), band.contains('B')};
	{
		QString testBand = band;
		testBand.remove('R');
		testBand.remove('G');
		testBand.remove('B');
		if (!testBand.isEmpty())
		{
			return cmd.error(QObject::tr("Invalid parameter: bands after \"-%1\" (expected: any combination of R, G or B)").arg(COMMAND_COLOR_LEVELS));
		}
	}

	// min level
	int levels[4] = {0};
	for (int i = 0; i < 4; ++i)
	{
		bool    ok       = true;
		QString levelStr = cmd.arguments().takeFirst();
		levels[i]        = levelStr.toInt(&ok);
		if (!ok || levels[i] < 0 || levels[i] > 255)
		{
			return cmd.error(QObject::tr("Invalid parameter: color level after \"-%1 COLOR-BANDS\" (integer value between 0 and 255 expected)").arg(COMMAND_COLOR_LEVELS));
		}
	}

	// process clouds
	if (!cmd.clouds().empty())
	{
		bool hasClouds = false;
		for (CLCloudDesc& desc : cmd.clouds())
		{
			if (desc.pc && desc.pc->hasColors())
			{
				if (!ccColorLevelsDlg::ScaleColorFields(desc.pc, levels[0], levels[1], levels[2], levels[3], rgb))
				{
					cmd.warning(QObject::tr("Failed to scale the color band(s) of cloud '%1'").arg(desc.pc->getName()));
				}
				else
				{
					hasClouds = true;
				}
			}
		}

		// save output
		if (hasClouds && cmd.autoSaveMode() && !cmd.saveClouds(QObject::tr("COLOR_LEVELS_%1_%2_%3").arg(band).arg(levels[2]).arg(levels[3])))
		{
			return false;
		}
	}

	if (!cmd.meshes().empty())
	{
		bool hasMeshes = false;
		for (CLMeshDesc& desc : cmd.meshes())
		{
			ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(desc.mesh);
			if (cloud && cloud->hasColors())
			{
				if (!ccColorLevelsDlg::ScaleColorFields(cloud, levels[0], levels[1], levels[2], levels[3], rgb))
				{
					cmd.warning(QObject::tr("Failed to scale the color band(s) of mesh '%1'").arg(desc.mesh->getName()));
				}
				else
				{
					hasMeshes = true;
				}
			}
			else if (desc.mesh->hasColors())
			{
				cmd.warning(QObject::tr("Vertices of mesh '%1' are locked (they may be shared by multiple entities for instance). Can't apply the current command on them.").arg(desc.mesh->getName()));
			}
		}

		// save output
		if (hasMeshes && cmd.autoSaveMode() && !cmd.saveMeshes(QObject::tr("COLOR_LEVELS_%1_%2_%3").arg(band).arg(levels[2]).arg(levels[3])))
		{
			return false;
		}
	}

	return true;
}

CommandDist::CommandDist(bool cloud2meshDist, const QString& name, const QString& keyword)
    : ccCommandLineInterface::Command(name, keyword)
    , m_cloud2meshDist(cloud2meshDist)
{
}

bool CommandDist::process(ccCommandLineInterface& cmd)
{
	// compared cloud
	CLEntityDesc* compEntity    = nullptr;
	ccHObject*    compCloud     = nullptr;
	size_t        nextMeshIndex = 0;
	if (cmd.clouds().empty())
	{
		// no cloud loaded
		if (!m_cloud2meshDist || cmd.meshes().size() < 2)
		{
			// we would need at least two meshes
			return cmd.error(QObject::tr("No point cloud available. Be sure to open or generate one first!"));
		}
		else
		{
			cmd.warning(QObject::tr("No point cloud available. Will use the first mesh vertices as compared cloud."));
			compEntity = &(cmd.meshes().front());
			compCloud  = dynamic_cast<ccPointCloud*>(cmd.meshes()[nextMeshIndex++].mesh->getAssociatedCloud());
			if (!compCloud)
			{
				return cmd.error(QObject::tr("Unhandled mesh vertices type"));
			}
		}
	}
	else // at least two clouds
	{
		if (m_cloud2meshDist && cmd.clouds().size() != 1)
		{
			cmd.warning(QObject::tr("[C2M] Multiple point clouds loaded! Will take the first one by default."));
		}
		compEntity = &(cmd.clouds().front());
		compCloud  = cmd.clouds().front().pc;
	}
	assert(compEntity && compCloud);

	// reference entity
	ccHObject* refEntity = nullptr;
	if (m_cloud2meshDist)
	{
		if (cmd.meshes().size() <= nextMeshIndex)
		{
			return cmd.error(QObject::tr("No mesh available. Be sure to open one first!"));
		}
		else if (cmd.meshes().size() != nextMeshIndex + 1)
		{
			cmd.warning(QString("Multiple meshes loaded! We take the %1 one by default").arg(nextMeshIndex == 0 ? "first" : "second"));
		}
		refEntity = cmd.meshes()[nextMeshIndex].mesh;
	}
	else
	{
		if (cmd.clouds().size() < 2)
		{
			return cmd.error(QObject::tr("Only one point cloud available. Be sure to open or generate a second one before performing C2C distance!"));
		}
		else if (cmd.clouds().size() > 2)
		{
			cmd.warning(QObject::tr("More than 3 point clouds loaded! We take the second one as reference by default"));
		}
		refEntity = cmd.clouds()[1].pc;
	}

	// inner loop for Distance computation options
	bool     flipNormals       = false;
	bool     unsignedDistances = false;
	bool     robust            = true;
	double   maxDist           = 0.0;
	unsigned octreeLevel       = 0;
	int      maxThreadCount    = 0;

	bool   splitXYZ   = false;
	bool   mergeXY    = false;
	int    modelIndex = 0;
	bool   useKNN     = true;
	double nSize      = 0;

	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2M_DIST_FLIP_NORMALS))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			flipNormals = true;

			if (!m_cloud2meshDist)
			{
				cmd.warning(QObject::tr("Parameter \"-%1\" ignored: only for C2M distance!").arg(COMMAND_C2M_DIST_FLIP_NORMALS));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2M_DIST_UNSIGNED))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			unsignedDistances = true;

			if (!m_cloud2meshDist)
			{
				cmd.warning(QObject::tr("Parameter \"-%1\" ignored: only for C2M distance!").arg(COMMAND_C2M_DIST_UNSIGNED));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2M_DIST_NON_ROBUST))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			robust = false;

			if (!m_cloud2meshDist)
			{
				cmd.warning(QObject::tr("Parameter \"-%1\" ignored: only for C2M distance!").arg(COMMAND_C2M_DIST_NON_ROBUST));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2X_MAX_DISTANCE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: value after \"-%1\"").arg(COMMAND_C2X_MAX_DISTANCE));
			}
			bool conversionOk = false;
			maxDist           = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: value after \"-%1\"").arg(COMMAND_C2X_MAX_DISTANCE));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2X_OCTREE_LEVEL))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: value after \"-%1\"").arg(COMMAND_C2X_OCTREE_LEVEL));
			}
			bool conversionOk = false;
			octreeLevel       = cmd.arguments().takeFirst().toUInt(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: value after \"-%1\"").arg(COMMAND_C2X_OCTREE_LEVEL));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2C_SPLIT_XYZ))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			splitXYZ = true;

			if (m_cloud2meshDist)
			{
				cmd.warning(QObject::tr("Parameter \"-%1\" ignored: only for C2C distance!"));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2C_SPLIT_XY_Z))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			splitXYZ = true;
			mergeXY  = true;

			if (m_cloud2meshDist)
			{
				cmd.warning(QObject::tr("Parameter \"-%1\" ignored: only for C2C distance!"));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2C_LOCAL_MODEL))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (!cmd.arguments().empty())
			{
				QString modelType = cmd.arguments().takeFirst().toUpper();
				if (modelType == "LS")
				{
					modelIndex = 1;
				}
				else if (modelType == "TRI")
				{
					modelIndex = 2;
				}
				else if (modelType == "HF")
				{
					modelIndex = 3;
				}
				else
				{
					return cmd.error(QObject::tr("Invalid parameter: unknown model type \"%1\"").arg(modelType));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing parameter: model type after \"-%1\" (LS/TRI/HF)").arg(COMMAND_C2C_LOCAL_MODEL));
			}

			if (!cmd.arguments().empty())
			{
				QString nType = cmd.arguments().takeFirst().toUpper();
				if (nType == "KNN")
				{
					useKNN = true;
				}
				else if (nType == "SPHERE")
				{
					useKNN = false;
				}
				else
				{
					return cmd.error(QObject::tr("Invalid parameter: unknown neighborhood type \"%1\"").arg(nType));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing parameter: expected neighborhood type after model type (KNN/SPHERE)"));
			}

			// neighborhood size
			if (!cmd.arguments().empty())
			{
				bool conversionOk = false;
				nSize             = cmd.arguments().takeFirst().toDouble(&conversionOk);
				if (!conversionOk)
				{
					return cmd.error(QObject::tr("Invalid parameter: neighborhood size"));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing parameter: expected neighborhood size after neighborhood type (neighbor count/sphere radius)"));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_MAX_THREAD_COUNT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: max thread count after '%1'").arg(COMMAND_MAX_THREAD_COUNT));
			}

			bool ok;
			maxThreadCount = cmd.arguments().takeFirst().toInt(&ok);
			if (!ok || maxThreadCount < 0)
			{
				return cmd.error(QObject::tr("Invalid thread count! (after %1)").arg(COMMAND_MAX_THREAD_COUNT));
			}
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	// spawn dialog (virtually) so as to prepare the comparison process
	ccComparisonDlg compDlg(compCloud,
	                        refEntity,
	                        m_cloud2meshDist ? ccComparisonDlg::CLOUDMESH_DIST : ccComparisonDlg::CLOUDCLOUD_DIST,
	                        cmd.widgetParent(),
	                        true);

	if (!compDlg.initDialog())
	{
		return cmd.error(QObject::tr("Failed to initialize comparison dialog"));
	}

	// update parameters
	if (maxDist > 0)
	{
		compDlg.maxDistCheckBox->setChecked(true);
		compDlg.maxSearchDistSpinBox->setValue(maxDist);
	}
	if (octreeLevel > 0)
	{
		compDlg.octreeLevelComboBox->setCurrentIndex(octreeLevel);
	}
	if (maxThreadCount != 0)
	{
		compDlg.maxThreadCountSpinBox->setValue(maxThreadCount);
	}

	if (m_cloud2meshDist)
	{
		// C2M-only parameters
		compDlg.flipNormalsCheckBox->setChecked(flipNormals);
		compDlg.signedDistCheckBox->setChecked(!unsignedDistances);
		compDlg.robustCheckBox->setChecked(robust);
	}
	else
	{
		// C2C-only parameters
		if (splitXYZ)
		{
			// DGM: not true anymore
			// if (maxDist > 0)
			//	cmd.warning("'Split XYZ' option is ignored if max distance is defined!");
			compDlg.split3DCheckBox->setChecked(true);
		}
		if (mergeXY)
			compDlg.compute2DCheckBox->setChecked(true);
		if (modelIndex != 0)
		{
			compDlg.localModelComboBox->setCurrentIndex(modelIndex);
			if (useKNN)
			{
				compDlg.lmKNNRadioButton->setChecked(true);
				compDlg.lmKNNSpinBox->setValue(static_cast<int>(nSize));
			}
			else
			{
				compDlg.lmRadiusRadioButton->setChecked(true);
				compDlg.lmRadiusDoubleSpinBox->setValue(nSize);
			}
		}
	}

	if (!compDlg.computeDistances())
	{
		compDlg.cancelAndExit();
		return cmd.error(QObject::tr("An error occurred during distances computation!"));
	}

	compDlg.applyAndExit();

	QString suffix(m_cloud2meshDist ? "_C2M_DIST" : "_C2C_DIST");
	if (maxDist > 0)
	{
		suffix += QObject::tr("_MAX_DIST_%1").arg(maxDist);
	}

	compEntity->basename += suffix;

	if (cmd.autoSaveMode())
	{
		QString errorStr = cmd.exportEntity(*compEntity);
		if (!errorStr.isEmpty())
		{
			return cmd.error(errorStr);
		}
	}

	return true;
}

CommandC2MDist::CommandC2MDist()
    : CommandDist(true, QObject::tr("C2M distance"), COMMAND_C2M_DIST)
{
}

CommandC2CDist::CommandC2CDist()
    : CommandDist(false, QObject::tr("C2C distance"), COMMAND_C2C_DIST)
{
}

CommandCPS::CommandCPS()
    : ccCommandLineInterface::Command(QObject::tr("Closest Point Set"), COMMAND_CLOSEST_POINT_SET)
{
}

bool CommandCPS::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().size() < 2)
	{
		return cmd.error(QObject::tr("At least two point clouds are needed to compute the closest point set!"));
	}
	else if (cmd.clouds().size() > 2)
	{
		cmd.warning(QObject::tr("More than 3 point clouds loaded! We take the second one as reference by default"));
	}

	// COMPARED CLOUD / REFERENCE CLOUD
	CLCloudDesc   compDesc       = cmd.clouds().front();
	CLCloudDesc   refDesc        = cmd.clouds()[1];
	ccPointCloud* compPointCloud = compDesc.pc;
	ccPointCloud* refPointCloud  = refDesc.pc;

	assert(compPointCloud && refPointCloud);

	ccProgressDialog                                                           pDlg(true, nullptr);
	CCCoreLib::DistanceComputationTools::Cloud2CloudDistancesComputationParams params;
	CCCoreLib::ReferenceCloud                                                  closestPointSet(refPointCloud);
	params.CPSet = &closestPointSet;

	// COMPUTE CLOUD 2 CLOUD DISTANCE, THIS INCLUDES THE CLOSEST POINT SET GENERATION
	int result = CCCoreLib::DistanceComputationTools::computeCloud2CloudDistances(compPointCloud, refPointCloud, params, &pDlg);

	if (result >= 0)
	{
		// the extracted CPS will get the attributes of the reference cloud
		ccPointCloud* newCloud = refPointCloud->partialClone(&closestPointSet);
		// give to newCloud a name similar to the one generated by the GUI Closest Point Set tool
		CLCloudDesc desc(
		    newCloud,
		    "[" + refDesc.basename + "]_CPSet(" + compDesc.basename + ")",
		    cmd.clouds()[0].path);
		QString errorStr = cmd.exportEntity(desc, QString(), nullptr, ccCommandLineInterface::ExportOption::ForceNoTimestamp);
		if (!errorStr.isEmpty())
		{
			cmd.error(errorStr);
		}
		// add cloud to the current pool
		cmd.clouds().push_back(desc);
	}

	return true;
}

CommandStatTest::CommandStatTest()
    : ccCommandLineInterface::Command(QObject::tr("Statistical test"), COMMAND_STAT_TEST)
{
}

bool CommandStatTest::process(ccCommandLineInterface& cmd)
{
	// distribution
	CCCoreLib::GenericDistribution* distrib = nullptr;
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: distribution type after \"-%1\" (GAUSS/WEIBULL)").arg(COMMAND_STAT_TEST));
		}

		QString distribStr = cmd.arguments().takeFirst().toUpper();
		if (distribStr == "GAUSS")
		{
			// mu
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: mean value after \"GAUSS\""));
			}
			bool   conversionOk = false;
			double mu           = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: mean value after \"GAUSS\""));
			}
			// sigma
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: sigma value after \"GAUSS\" {mu}"));
			}
			conversionOk = false;
			double sigma = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: sigma value after \"GAUSS\" {mu}"));
			}

			CCCoreLib::NormalDistribution* N = new CCCoreLib::NormalDistribution();
			N->setParameters(static_cast<ScalarType>(mu), static_cast<ScalarType>(sigma * sigma)); // warning: we input sigma2 here (not sigma)
			distrib = static_cast<CCCoreLib::GenericDistribution*>(N);
		}
		else if (distribStr == "WEIBULL")
		{
			// a
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: a value after \"WEIBULL\""));
			}
			bool   conversionOk = false;
			double a            = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: a value after \"WEIBULL\""));
			}
			// b
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: b value after \"WEIBULL\" {a}"));
			}
			conversionOk = false;
			double b     = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: b value after \"WEIBULL\" {a}"));
			}
			// c
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: shift value after \"WEIBULL\" {a} {b}"));
			}
			conversionOk = false;
			double shift = cmd.arguments().takeFirst().toDouble(&conversionOk);
			if (!conversionOk)
			{
				return cmd.error(QObject::tr("Invalid parameter: shift value after \"WEIBULL\" {a} {b}"));
			}

			CCCoreLib::WeibullDistribution* N = new CCCoreLib::WeibullDistribution();
			N->setParameters(static_cast<ScalarType>(a), static_cast<ScalarType>(b), static_cast<ScalarType>(shift));
			distrib = static_cast<CCCoreLib::GenericDistribution*>(N);
		}
		else
		{
			return cmd.error(QObject::tr("Invalid parameter: unknown distribution \"%1\"").arg(distribStr));
		}
	}

	// pValue
	double pValue = 0.0005;
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: p-value after distribution"));
		}
		bool conversionOk = false;
		pValue            = cmd.arguments().takeFirst().toDouble(&conversionOk);
		if (!conversionOk)
		{
			return cmd.error(QObject::tr("Invalid parameter: p-value after distribution"));
		}
	}

	// kNN
	unsigned kNN = 16;
	{
		if (cmd.arguments().empty())
		{
			return cmd.error(QObject::tr("Missing parameter: neighbors after p-value"));
		}
		bool conversionOk = false;
		kNN               = cmd.arguments().takeFirst().toUInt(&conversionOk);
		if (!conversionOk)
		{
			return cmd.error(QObject::tr("Invalid parameter: neighbors after p-value"));
		}
	}

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No cloud available. Be sure to open one first!"));
	}

	QScopedPointer<ccProgressDialog> progressDialog(nullptr);
	if (!cmd.silentMode())
	{
		progressDialog.reset(new ccProgressDialog(false, cmd.widgetParent()));
		progressDialog->setAutoClose(false);
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		// we apply method on currently 'output' SF
		CCCoreLib::ScalarField* outSF = desc.pc->getCurrentOutScalarField();
		if (outSF)
		{
			assert(outSF->capacity() != 0);

			// force Chi2 Distances field as 'IN' field (create it by the way if necessary)
			int chi2SfIdx = desc.pc->getScalarFieldIndexByName(CC_CHI2_DISTANCES_DEFAULT_SF_NAME);
			if (chi2SfIdx < 0)
			{
				chi2SfIdx = desc.pc->addScalarField(CC_CHI2_DISTANCES_DEFAULT_SF_NAME);
			}
			if (chi2SfIdx < 0)
			{
				delete distrib;
				return cmd.error(QObject::tr("Couldn't allocate a new scalar field for computing chi2 distances! Try to free some memory ..."));
			}
			desc.pc->setCurrentInScalarField(chi2SfIdx);

			// compute octree if necessary
			ccOctree::Shared theOctree = desc.pc->getOctree();
			if (!theOctree)
			{
				theOctree = desc.pc->computeOctree(progressDialog.data());
				if (!theOctree)
				{
					delete distrib;
					cmd.error(QObject::tr("Couldn't compute octree for cloud '%1'!").arg(desc.pc->getName()));
					break;
				}
			}

			double chi2dist = CCCoreLib::StatisticalTestingTools::testCloudWithStatisticalModel(distrib, desc.pc, kNN, pValue, progressDialog.data(), theOctree.data());

			cmd.print(QObject::tr("[Chi2 Test] %1 test result = %2").arg(distrib->getName()).arg(chi2dist));

			// we set the theoretical Chi2 distance limit as the minimum displayed SF value so that all points below are grayed
			{
				ccScalarField* chi2SF = static_cast<ccScalarField*>(desc.pc->getCurrentInScalarField());
				assert(chi2SF);
				chi2SF->computeMinAndMax();
				chi2dist *= chi2dist;
				chi2SF->setMinDisplayed(static_cast<ScalarType>(chi2dist));
				chi2SF->setSymmetricalScale(false);
				chi2SF->setSaturationStart(static_cast<ScalarType>(chi2dist));
				// chi2SF->setSaturationStop(chi2dist);
				desc.pc->setCurrentDisplayedScalarField(chi2SfIdx);
				desc.pc->showSF(true);
			}

			desc.basename += QObject::tr("_STAT_TEST_%1").arg(distrib->getName());
			if (cmd.autoSaveMode())
			{
				QString errorStr = cmd.exportEntity(desc);
				if (!errorStr.isEmpty())
				{
					return cmd.error(errorStr);
				}
			}
		}
	}

	if (progressDialog)
	{
		progressDialog->close();
		QCoreApplication::processEvents();
	}

	return true;
}

CommandDelaunayTri::CommandDelaunayTri()
    : ccCommandLineInterface::Command(QObject::tr("Delaunay triangulation"), COMMAND_DELAUNAY)
{
}

bool CommandDelaunayTri::process(ccCommandLineInterface& cmd)
{
	bool   axisAligned   = true;
	double maxEdgeLength = 0;

	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_DELAUNAY_AA))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			axisAligned = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_DELAUNAY_BF))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			axisAligned = false;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_DELAUNAY_MAX_EDGE_LENGTH))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: max edge length value after '%1'").arg(COMMAND_DELAUNAY_MAX_EDGE_LENGTH));
			}
			bool ok;
			maxEdgeLength = cmd.arguments().takeFirst().toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid value for max edge length (%1)! (after %2)").arg(maxEdgeLength).arg(COMMAND_DELAUNAY_MAX_EDGE_LENGTH));
			}
		}
		else
		{
			break;
		}
	}

	cmd.print(QObject::tr("Axis aligned: %1").arg(axisAligned ? "yes" : "no"));

	// try to triangulate each cloud
	for (CLCloudDesc& desc : cmd.clouds())
	{
		cmd.print(QObject::tr("\tProcessing cloud %1").arg(!desc.pc->getName().isEmpty() ? desc.pc->getName() : "no name"));

		ccMesh* mesh = ccMesh::Triangulate(desc.pc,
		                                   axisAligned ? CCCoreLib::DELAUNAY_2D_AXIS_ALIGNED : CCCoreLib::DELAUNAY_2D_BEST_LS_PLANE,
		                                   false,
		                                   static_cast<PointCoordinateType>(maxEdgeLength),
		                                   2 // XY plane by default
		);

		if (mesh)
		{
			cmd.print(QObject::tr("\tResulting mesh: #%1 faces, %2 vertices").arg(mesh->size()).arg(mesh->getAssociatedCloud()->size()));

			CLMeshDesc meshDesc;
			{
				meshDesc.mesh        = mesh;
				meshDesc.basename    = desc.basename;
				meshDesc.path        = desc.path;
				meshDesc.indexInFile = desc.indexInFile;
			}

			// save mesh
			if (cmd.autoSaveMode())
			{
				QString outputFilename;
				QString errorStr = cmd.exportEntity(meshDesc, "DELAUNAY", &outputFilename);
				if (!errorStr.isEmpty())
				{
					cmd.warning(errorStr);
				}
			}

			// add the resulting mesh to the main set
			cmd.meshes().push_back(meshDesc);

			// the mesh takes ownership of the cloud.
			// Therefore we have to remove all clouds from the 'cloud set'! (see below)
			//(otherwise bad things will happen when we'll clear it later ;)
			desc.pc->setEnabled(false);
			mesh->addChild(desc.pc);
		}
	}
	// mehses have taken ownership of the clouds!
	cmd.clouds().resize(0);

	return true;
}

CommandSFArithmetic::CommandSFArithmetic()
    : ccCommandLineInterface::Command(QObject::tr("SF arithmetic"), COMMAND_SF_ARITHMETIC)
{
}

bool CommandSFArithmetic::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 2)
	{
		return cmd.error(QObject::tr("Missing parameter(s): SF index and/or operation after '%1' (2 values expected)").arg(COMMAND_SF_ARITHMETIC));
	}

	// read SF index
	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	// read operation type
	ccScalarFieldArithmeticsDlg::Operation operation = ccScalarFieldArithmeticsDlg::INVALID;
	{
		QString opName = cmd.arguments().takeFirst();
		operation      = ccScalarFieldArithmeticsDlg::GetOperationByName(opName);
		if (operation == ccScalarFieldArithmeticsDlg::INVALID)
		{
			return cmd.error(QObject::tr("Unknown operation! (%1)").arg(opName));
		}
		else if (operation <= ccScalarFieldArithmeticsDlg::MAX || operation == ccScalarFieldArithmeticsDlg::SET)
		{
			return cmd.error(QObject::tr("Operation %1 can't be applied with %2. Consider using the %3 command").arg(opName, COMMAND_SF_ARITHMETIC, COMMAND_SF_OP));
		}
	}

	bool inPlace = false;

	// read the optional arguments
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_SF_ARITHMETIC_IN_PLACE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			inPlace = true;
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	// apply operation on clouds
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFIndex = GetScalarFieldIndex(desc.pc, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				if (!ccScalarFieldArithmeticsDlg::Apply(desc.pc, operation, thisSFIndex, inPlace))
				{
					return cmd.error(QObject::tr("Failed to apply operation on cloud '%1'").arg(desc.pc->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc, "SF_ARITHMETIC");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	// and meshes!
	for (size_t j = 0; j < cmd.meshes().size(); ++j)
	{
		bool           isLocked = false;
		ccGenericMesh* mesh     = cmd.meshes()[j].mesh;
		ccPointCloud*  cloud    = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
		if (cloud && !isLocked)
		{
			int thisSFIndex = GetScalarFieldIndex(cloud, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				if (!ccScalarFieldArithmeticsDlg::Apply(cloud, operation, thisSFIndex, inPlace))
				{
					return cmd.error(QObject::tr("Failed to apply operation on mesh '%1'").arg(mesh->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(cmd.meshes()[j], "SF_ARITHMETIC");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	return true;
}

CommandSFOperation::CommandSFOperation()
    : ccCommandLineInterface::Command(QObject::tr("SF operation"), COMMAND_SF_OP)
{
}

bool CommandSFOperation::process(ccCommandLineInterface& cmd)
{
	// in place modifier, to keep old commands intact we should keep it in place by default. However it makes the command line inconsistent, because the SF_ARITHMETIC works the other way.
	bool inPlace = true;
	if (!cmd.arguments().empty())
	{
		if (cmd.IsCommand(cmd.arguments().front(), COMMAND_SF_OP_NOT_IN_PLACE))
		{
			// local arg detected
			inPlace = false;
			cmd.arguments().pop_front();
		}
	}

	if (cmd.arguments().size() < 3)
	{
		return cmd.error(QObject::tr("Missing parameter(s): SF index and/or operation and/or scalar value after '%1' (3 values expected)").arg(COMMAND_SF_OP));
	}
	// read SF index
	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	// read operation type
	ccScalarFieldArithmeticsDlg::Operation operation = ccScalarFieldArithmeticsDlg::INVALID;
	{
		QString opName = cmd.arguments().takeFirst();
		operation      = ccScalarFieldArithmeticsDlg::GetOperationByName(opName);
		if (operation == ccScalarFieldArithmeticsDlg::INVALID)
		{
			return cmd.error(QObject::tr("Unknown operation! (%1)").arg(opName));
		}
		else if (operation > ccScalarFieldArithmeticsDlg::MAX && operation != ccScalarFieldArithmeticsDlg::SET)
		{
			return cmd.error(QObject::tr("Operation %1 can't be applied with %2. Consider using the %3 command").arg(opName, COMMAND_SF_OP, COMMAND_SF_ARITHMETIC));
		}
	}

	// read scalar value
	ScalarType           value        = static_cast<ScalarType>(1.0);
	USE_SPECIAL_SF_VALUE specialValue = USE_SPECIAL_SF_VALUE::USE_NONE;
	{
		QString valueStr = cmd.arguments().takeFirst();
		specialValue     = ToSpecialSFValue(valueStr);
		if (specialValue == USE_NONE)
		{
			bool ok = false;
			value   = valueStr.toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid scalar value! (after %1)").arg(COMMAND_SF_OP));
			}
		}
	}

	ccScalarFieldArithmeticsDlg::SF2 sf2;
	{
		sf2.isConstantValue = true;
		sf2.constantValue   = value;
	}

	// apply operation on clouds
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFIndex = GetScalarFieldIndex(desc.pc, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				sf2.constantValue = GetSFValue(*desc.pc, thisSFIndex, value, specialValue);

				if (!ccScalarFieldArithmeticsDlg::Apply(desc.pc, operation, thisSFIndex, inPlace, &sf2))
				{
					return cmd.error(QObject::tr("Failed to apply operation on cloud '%1'").arg(desc.pc->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc, "SF_OP");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	// and meshes!
	for (size_t j = 0; j < cmd.meshes().size(); ++j)
	{
		bool           isLocked = false;
		ccGenericMesh* mesh     = cmd.meshes()[j].mesh;
		ccPointCloud*  cloud    = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
		if (cloud && !isLocked)
		{
			int thisSFIndex = GetScalarFieldIndex(cloud, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				sf2.constantValue = GetSFValue(*cloud, thisSFIndex, value, specialValue);

				if (!ccScalarFieldArithmeticsDlg::Apply(cloud, operation, thisSFIndex, inPlace, &sf2))
				{
					return cmd.error(QObject::tr("Failed to apply operation on mesh '%1'").arg(mesh->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(cmd.meshes()[j], "SF_OP");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	return true;
}

CommandSFOperationSF::CommandSFOperationSF()
    : ccCommandLineInterface::Command(QObject::tr("SF (add, sub, mult, div) SF"), COMMAND_SF_OP_SF)
{
}

bool CommandSFOperationSF::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 3)
	{
		return cmd.error(QObject::tr("Missing parameter(s): SF index and operation and SF index '%1' (3 values expected)").arg(COMMAND_SF_OP_SF));
	}

	// read SF index 1
	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName))
	{
		return false;
	}

	// read operation type
	ccScalarFieldArithmeticsDlg::Operation operation = ccScalarFieldArithmeticsDlg::INVALID;
	{
		QString opName = cmd.arguments().takeFirst();
		operation      = ccScalarFieldArithmeticsDlg::GetOperationByName(opName);
		if (operation == ccScalarFieldArithmeticsDlg::INVALID)
		{
			return cmd.error(QObject::tr("Unknown operation! (%1)").arg(opName));
		}
		else if (operation > ccScalarFieldArithmeticsDlg::MAX)
		{
			return cmd.error(QObject::tr("Operation %1 can't be applied with %2").arg(opName, COMMAND_SF_OP_SF));
		}
	}

	// read SF index 2
	int     sfIndex2 = -1;
	QString sfName2;
	if (!GetSFIndexOrName(cmd, sfIndex2, sfName2))
	{
		return false;
	}

	// apply operation on clouds
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFFIndex  = GetScalarFieldIndex(desc.pc, sfIndex, sfName);
			int thisSFFIndex2 = GetScalarFieldIndex(desc.pc, sfIndex2, sfName2);
			if (thisSFFIndex >= 0 && thisSFFIndex2 >= 0)
			{
				ccScalarFieldArithmeticsDlg::SF2 sf2;
				{
					sf2.isConstantValue = false;
					sf2.sfIndex         = thisSFFIndex2;
				}

				if (!ccScalarFieldArithmeticsDlg::Apply(desc.pc, operation, thisSFFIndex, true, &sf2))
				{
					return cmd.error(QObject::tr("Failed to apply operation on cloud '%1'").arg(desc.pc->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc, "SF_OP_SF");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	// and meshes!
	for (size_t j = 0; j < cmd.meshes().size(); ++j)
	{
		bool           isLocked = false;
		ccGenericMesh* mesh     = cmd.meshes()[j].mesh;
		ccPointCloud*  cloud    = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
		if (cloud && !isLocked)
		{
			int thisSFFIndex  = GetScalarFieldIndex(cloud, sfIndex, sfName);
			int thisSFFIndex2 = GetScalarFieldIndex(cloud, sfIndex2, sfName2);
			if (thisSFFIndex >= 0 && thisSFFIndex2 >= 0)
			{
				ccScalarFieldArithmeticsDlg::SF2 sf2;
				{
					sf2.isConstantValue = false;
					sf2.sfIndex         = thisSFFIndex2;
				}

				if (!ccScalarFieldArithmeticsDlg::Apply(cloud, operation, thisSFFIndex, true, &sf2))
				{
					return cmd.error(QObject::tr("Failed to apply operation on mesh '%1'").arg(mesh->getName()));
				}
				else if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(cmd.meshes()[j], "SF_OP_SF");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	return true;
}

CommandSFInterpolation::CommandSFInterpolation()
    : ccCommandLineInterface::Command(QObject::tr("SF interpolation"), COMMAND_SF_INTERP)
{
}

bool CommandSFInterpolation::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 1)
		return cmd.error(QObject::tr("Missing parameter(s): SF index after '%1' (1 value expected)").arg(COMMAND_SF_INTERP));

	if (cmd.clouds().size() < 2)
		return cmd.error(QObject::tr("Unexpected number of clouds for '%1' (at least 2 clouds expected: first = source, second = dest)").arg(COMMAND_SF_INTERP));

	// read sf index or name
	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	bool destIsFirst = false;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_SF_INTERP_DEST_IS_FIRST))
		{
			cmd.print(QObject::tr("[DEST_IS_FIRST]"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			destIsFirst = true;
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	ccPointCloud* source = cmd.clouds()[0].pc;
	ccPointCloud* dest   = cmd.clouds()[1].pc;
	if (destIsFirst) // swap source and destination
	{
		source = cmd.clouds()[1].pc;
		dest   = cmd.clouds()[0].pc;
	}

	sfIndex = GetScalarFieldIndex(source, sfIndex, sfName, true);
	if (sfIndex < 0)
	{
		return false;
	}

	cmd.print("SF to interpolate: index " + QString::number(sfIndex) + ", name " + QString::fromStdString(source->getScalarField(sfIndex)->getName()));

	// semi-persistent parameters
	ccPointCloudInterpolator::Parameters params;
	{
		params.method = ccPointCloudInterpolator::Parameters::NEAREST_NEIGHBOR; // nearest neighbor
		params.algo   = ccPointCloudInterpolator::Parameters::NORMAL_DIST;      // normal distribution
		params.knn    = 6;
		params.radius = static_cast<float>(dest->getOwnBB().getDiagNormd() / 100);
		params.sigma  = params.radius / 2.5; // see ccInterpolationDlg::onRadiusUpdated
	}

	return ccEntityAction::interpolateSFs(source, dest, sfIndex, params, cmd.widgetParent());
}

CommandColorInterpolation::CommandColorInterpolation()
    : ccCommandLineInterface::Command(QObject::tr("Color interpolation"), COMMAND_COLOR_INTERP)
{
}

bool CommandColorInterpolation::process(ccCommandLineInterface& cmd)
{
	if (cmd.clouds().size() < 2)
		return cmd.error(QObject::tr("Unexpected number of clouds for '%1' (at least 2 clouds expected: first = source, second = dest)").arg(COMMAND_COLOR_INTERP));

	ccHObject::Container entities;
	entities.push_back(cmd.clouds()[0].pc);
	entities.push_back(cmd.clouds()[1].pc);

	return ccEntityAction::interpolateColors(entities, cmd.widgetParent());
}

CommandFilter::CommandFilter()
    : ccCommandLineInterface::Command(QObject::tr("FILTER"), COMMAND_FILTER)
{
}

bool CommandFilter::process(ccCommandLineInterface& cmd)
{
	bool applyToRGB = false;
	bool applyToSF  = false;
	bool gaussian   = false;
	ccPointCloud::RgbFilterOptions(filterParams);
	filterParams.commandLine = true;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, OPTION_SF))
		{
			cmd.arguments().pop_front();
			applyToSF = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_RGB))
		{
			cmd.arguments().pop_front();
			applyToRGB = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_BILATERAL))
		{
			cmd.arguments().pop_front();
			if (filterParams.filterType == ccPointCloud::RGB_FILTER_TYPES::NONE)
			{
				filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::BILATERAL;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_GAUSSIAN))
		{
			cmd.arguments().pop_front();
			if (filterParams.filterType == ccPointCloud::RGB_FILTER_TYPES::NONE)
			{
				filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::GAUSSIAN;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_MEAN))
		{
			cmd.arguments().pop_front();
			if (filterParams.filterType == ccPointCloud::RGB_FILTER_TYPES::NONE)
			{
				filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::MEAN;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_MEDIAN))
		{
			cmd.arguments().pop_front();
			if (filterParams.filterType == ccPointCloud::RGB_FILTER_TYPES::NONE)
			{
				filterParams.filterType = ccPointCloud::RGB_FILTER_TYPES::MEDIAN;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_SIGMA))
		{
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: spatial sigma after '-%1'").arg(OPTION_SIGMA));
			}

			bool ok                   = false;
			filterParams.spatialSigma = cmd.arguments().takeFirst().toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid value for spatial sigma after '%1'!").arg(OPTION_SIGMA));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_SIGMA_SF))
		{
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: spatial sigma after '-%1'").arg(OPTION_SIGMA_SF));
			}

			bool ok              = false;
			filterParams.sigmaSF = cmd.arguments().takeFirst().toDouble(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid value for spatial sigma after '%1'!").arg(OPTION_SIGMA_SF));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_BURNT_COLOR_THRESHOLD))
		{
			cmd.arguments().pop_front();
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: burnt color threshold after '-%1'").arg(OPTION_BURNT_COLOR_THRESHOLD));
			}

			bool ok                     = false;
			uint burntOutColorThreshold = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok || burntOutColorThreshold > 255)
			{
				return cmd.error(QObject::tr("Invalid value for burnt color threshold after '%1', must be an integer between 0 and 255!").arg(OPTION_BURNT_COLOR_THRESHOLD));
			}
			filterParams.burntOutColorThreshold = static_cast<unsigned char>(burntOutColorThreshold);
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_BLEND_GRAYSCALE))
		{
			cmd.arguments().pop_front();
			if (cmd.arguments().size() < 2)
			{
				return cmd.error(QObject::tr("Missing parameter: blend grayscale threshold and grayscale percent after '-%1'").arg(OPTION_BLEND_GRAYSCALE));
			}

			bool ok             = false;
			uint blendGrayscale = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok || blendGrayscale > 255)
			{
				return cmd.error(QObject::tr("Invalid value for blend grayscale threshold after '%1', must be an integer between 0 and 255!").arg(OPTION_BLEND_GRAYSCALE));
			}
			filterParams.blendGrayscale          = true;
			filterParams.blendGrayscaleThreshold = static_cast<unsigned char>(blendGrayscale);

			uint grayscalePercent = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok || grayscalePercent > 100)
			{
				return cmd.error(QObject::tr("Invalid value for grayscale percent after '%1 %2', must be an integer between 0 and 100!").arg(OPTION_BLEND_GRAYSCALE).arg(filterParams.blendGrayscaleThreshold));
			}
			filterParams.blendGrayscalePercent = static_cast<double>(grayscalePercent) / 100;
		}

		else
		{
			break;
		}
	}
	if (!applyToRGB && !applyToSF)
	{
		return cmd.error(QObject::tr("Missing parameter -%1 and/or -%2 need to be set.").arg(OPTION_RGB).arg(OPTION_SF));
	}

	if (filterParams.filterType == ccPointCloud::RGB_FILTER_TYPES::NONE)
	{
		return cmd.error(QObject::tr("Missing parameter any of '-%1', '-%2', '-%3', '-%4' need to be set.")
		                     .arg(OPTION_MEAN)
		                     .arg(OPTION_GAUSSIAN)
		                     .arg(OPTION_BILATERAL)
		                     .arg(OPTION_MEDIAN));
	}

	// apply operation on clouds
	ccHObject::Container selectedEntities;

	for (CLCloudDesc& thisCloudDesc : cmd.clouds())
	{
		selectedEntities.push_back(thisCloudDesc.pc);
	}

	if (applyToSF && applyToRGB)
	{
		applyToSF                       = false;
		filterParams.applyToSFduringRGB = true;
	}
	if (applyToSF)
	{
		return ccEntityAction::sfGaussianFilter(selectedEntities, filterParams, cmd.widgetParent());
	}
	else if (applyToRGB)
	{
		return ccEntityAction::rgbGaussianFilter(selectedEntities, filterParams, cmd.widgetParent());
	}

	return true;
}

CommandRenameEntities::CommandRenameEntities()
    : ccCommandLineInterface::Command(QObject::tr("Rename entities"), COMMAND_RENAME_ENTITIES)
{
}

bool CommandRenameEntities::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: Name after \"-%1\"").arg(COMMAND_RENAME_ENTITIES));
	}

	QString newBaseName = cmd.arguments().takeFirst();
	// Validate if the given name contains any breaking characters for NTFS filesystem at least
	QRegExp          rx("[^:/\\\\*?\"|<>]*");
	QRegExpValidator v(rx, 0);
	int              pos = 0;
	if (!v.validate(newBaseName, pos))
	{
		assert(false);
		return cmd.error("Name cannot contain any of these characters: :/\\*?\"|<>");
	}

	// apply operation on clouds
	int    index        = 1;
	size_t nrOfEntities = cmd.clouds().size() + cmd.meshes().size();
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			QString currentCloudName = newBaseName;
			if (nrOfEntities > 1)
			{
				currentCloudName += "-" + QString::number(index).rightJustified(3, '0');
			}
			cmd.print("Cloud '" + desc.pc->getName() + "' renamed to '" + currentCloudName + "'");
			// rename the Point Cloud entity
			desc.pc->setName(currentCloudName);
			// rename the Root entity
			desc.basename = currentCloudName;
			++index;
		}
	}

	// apply operation on meshes
	for (CLMeshDesc& desc : cmd.meshes())
	{
		if (desc.mesh)
		{
			QString currentMeshName = newBaseName;
			if (nrOfEntities > 1)
			{
				currentMeshName += "-" + QString::number(index).rightJustified(3, '0');
			}
			cmd.print("Mesh '" + desc.mesh->getName() + "' renamed to '" + currentMeshName + "'");
			// rename the Mesh entity
			desc.mesh->setName(currentMeshName);
			// rename the Root entity
			desc.basename = currentMeshName;
			++index;
		}
	}

	return true;
}

CommandSFRename::CommandSFRename()
    : ccCommandLineInterface::Command(QObject::tr("Rename SF"), COMMAND_RENAME_SF)
{
}

bool CommandSFRename::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().size() < 2)
	{
		return cmd.error(QObject::tr("Missing parameter(s): SF index and/or scalar field name after '%1' (2 values expected)").arg(COMMAND_RENAME_SF));
	}

	// read SF index
	int     sfIndex = -1;
	QString sfName;
	if (!GetSFIndexOrName(cmd, sfIndex, sfName, true))
	{
		return false;
	}

	// read the SF name
	QString newSFName = cmd.arguments().takeFirst();

	// apply operation on clouds
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			int thisSFIndex = GetScalarFieldIndex(desc.pc, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				int indexOfSFWithSameName = desc.pc->getScalarFieldIndexByName(newSFName.toStdString());
				if (indexOfSFWithSameName >= 0 && thisSFIndex != indexOfSFWithSameName)
				{
					return cmd.error("A SF with the same name is already defined on cloud " + desc.pc->getName());
				}
				CCCoreLib::ScalarField* sf = desc.pc->getScalarField(thisSFIndex);
				if (!sf)
				{
					assert(false);
					return cmd.error("Internal error: invalid SF index");
				}
				sf->setName(newSFName.toStdString());

				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc, "SF_RENAMED");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	// and meshes!
	for (CLMeshDesc& desc : cmd.meshes())
	{
		bool          isLocked = false;
		ccPointCloud* cloud    = ccHObjectCaster::ToPointCloud(desc.mesh, &isLocked);
		if (cloud && !isLocked)
		{
			int thisSFIndex = GetScalarFieldIndex(cloud, sfIndex, sfName, true);
			if (thisSFIndex >= 0)
			{
				int indexOfSFWithSameName = cloud->getScalarFieldIndexByName(newSFName.toStdString());
				if (indexOfSFWithSameName >= 0 && thisSFIndex != indexOfSFWithSameName)
				{
					return cmd.error("A SF with the same name is already defined on cloud " + cloud->getName());
				}
				CCCoreLib::ScalarField* sf = cloud->getScalarField(thisSFIndex);
				if (!sf)
				{
					assert(false);
					return cmd.error("Internal error: invalid SF index");
				}
				sf->setName(newSFName.toStdString());

				if (cmd.autoSaveMode())
				{
					QString errorStr = cmd.exportEntity(desc, "SF_RENAMED");
					if (!errorStr.isEmpty())
					{
						return cmd.error(errorStr);
					}
				}
			}
		}
	}

	return true;
}

CommandSFAddConst::CommandSFAddConst()
    : ccCommandLineInterface::Command(QObject::tr("Add constant SF"), COMMAND_SF_ADD_CONST)
{
}

bool CommandSFAddConst::process(ccCommandLineInterface& cmd)
{
	cmd.print(QObject::tr("Note: this operation is only done on clouds"));

	if (cmd.arguments().size() < 2)
	{
		return cmd.error(QObject::tr("Missing parameter(s): SF name and value after '%1' (2 values expected)").arg(COMMAND_SF_ADD_CONST));
	}

	// read the SF name
	QString sfName = cmd.arguments().takeFirst();

	// read constant value
	bool       ok    = true;
	ScalarType value = static_cast<ScalarType>(cmd.arguments().takeFirst().toDouble(&ok));
	if (!ok)
	{
		return cmd.error(QObject::tr("Invalid constant value! (after %1)").arg(COMMAND_SF_ADD_CONST));
	}

	// apply operation on clouds
	for (CLCloudDesc& desc : cmd.clouds())
	{
		if (desc.pc)
		{
			// check that there is no existing scalar field with the same name
			int indexOfSFWithSameName = desc.pc->getScalarFieldIndexByName(sfName.toStdString());
			if (indexOfSFWithSameName >= 0)
				return cmd.error("A SF with the same name is already defined on cloud " + desc.pc->getName());

			// add the new scalar field
			int sfIndex = desc.pc->addScalarField(sfName.toStdString());
			if (sfIndex == -1)
			{
				return cmd.error("Internal error: addScalarField failed");
			}
			CCCoreLib::ScalarField* sf = desc.pc->getScalarField(sfIndex);
			assert(sf);
			for (unsigned index = 0; index < desc.pc->size(); index++)
			{
				sf->setValue(index, value);
			}

			if (cmd.autoSaveMode())
			{
				QString errorStr = cmd.exportEntity(desc, "SF_ADDED");
				if (!errorStr.isEmpty())
				{
					return cmd.error(errorStr);
				}
			}
		}
	}

	return true;
}

CommandSFAddId::CommandSFAddId()
    : ccCommandLineInterface::Command(QObject::tr("Add indexes as SF"), COMMAND_SF_ADD_ID)
{
}

bool CommandSFAddId::process(ccCommandLineInterface& cmd)
{
	ccHObject::Container selectedEntities;

	bool addIdAsInt = false;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_SF_ADD_ID_AS_INT))
		{
			cmd.print(QObject::tr("[AS_INT]"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			addIdAsInt = true;
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	for (CLCloudDesc& desc : cmd.clouds())
	{
		selectedEntities.push_back(desc.getEntity());
	}

	return ccEntityAction::sfAddIdField(selectedEntities, addIdAsInt);
}

CommandICP::CommandICP()
    : ccCommandLineInterface::Command("ICP", COMMAND_ICP)
{
}

bool CommandICP::process(ccCommandLineInterface& cmd)
{
	// look for local options
	bool                                              referenceIsFirst           = false;
	bool                                              adjustScale                = false;
	bool                                              enableFarthestPointRemoval = false;
	double                                            minErrorDiff               = 1.0e-6;
	unsigned                                          iterationCount             = 0;
	unsigned                                          randomSamplingLimit        = 20000;
	unsigned                                          overlap                    = 100;
	int                                               modelWeightsSFIndex        = -1;
	QString                                           modelWeightsSFIndexName;
	int                                               dataWeightsSFIndex = -1;
	QString                                           dataWeightsSFIndexName;
	int                                               maxThreadCount        = 0;
	int                                               transformationFilters = CCCoreLib::RegistrationTools::SKIP_NONE;
	bool                                              useC2MDistances       = false;
	bool                                              robustC2MDistances    = true;
	CCCoreLib::ICPRegistrationTools::NORMALS_MATCHING normalsMatching       = CCCoreLib::ICPRegistrationTools::NO_NORMAL;

	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_REFERENCE_IS_FIRST))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			referenceIsFirst = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_ADJUST_SCALE))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			adjustScale = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_ENABLE_FARTHEST_REMOVAL))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			enableFarthestPointRemoval = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_MIN_ERROR_DIIF))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: min error difference after '%1'").arg(COMMAND_ICP_MIN_ERROR_DIIF));
			}
			bool ok;
			minErrorDiff = cmd.arguments().takeFirst().toDouble(&ok);
			if (!ok || minErrorDiff <= 0)
			{
				return cmd.error(QObject::tr("Invalid value for min. error difference! (after %1)").arg(COMMAND_ICP_MIN_ERROR_DIIF));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_ITERATION_COUNT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: number of iterations after '%1'").arg(COMMAND_ICP_ITERATION_COUNT));
			}
			bool    ok;
			QString arg    = cmd.arguments().takeFirst();
			iterationCount = arg.toUInt(&ok);
			if (!ok || iterationCount == 0)
				return cmd.error(QObject::tr("Invalid number of iterations! (%1)").arg(arg));
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_OVERLAP))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: overlap percentage after '%1'").arg(COMMAND_ICP_OVERLAP));
			}
			bool    ok;
			QString arg = cmd.arguments().takeFirst();
			overlap     = arg.toUInt(&ok);
			if (!ok || overlap < 10 || overlap > 100)
			{
				return cmd.error(QObject::tr("Invalid overlap value! (%1 --> should be between 10 and 100)").arg(arg));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_RANDOM_SAMPLING_LIMIT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: random sampling limit value after '%1'").arg(COMMAND_ICP_RANDOM_SAMPLING_LIMIT));
			}
			bool ok;
			randomSamplingLimit = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok || randomSamplingLimit < 3)
			{
				return cmd.error(QObject::tr("Invalid random sampling limit! (after %1)").arg(COMMAND_ICP_RANDOM_SAMPLING_LIMIT));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_USE_MODEL_SF_AS_WEIGHT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: SF index after '%1'").arg(COMMAND_ICP_USE_MODEL_SF_AS_WEIGHT));
			}

			// read SF index
			if (!GetSFIndexOrName(cmd, modelWeightsSFIndex, modelWeightsSFIndexName, true))
			{
				return false;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_USE_DATA_SF_AS_WEIGHT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: SF index after '%1'").arg(COMMAND_ICP_USE_DATA_SF_AS_WEIGHT));
			}

			// read SF index
			if (!GetSFIndexOrName(cmd, dataWeightsSFIndex, dataWeightsSFIndexName, true))
			{
				return false;
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_MAX_THREAD_COUNT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: max thread count after '%1'").arg(COMMAND_MAX_THREAD_COUNT));
			}

			bool ok;
			maxThreadCount = cmd.arguments().takeFirst().toInt(&ok);
			if (!ok || maxThreadCount < 0)
			{
				return cmd.error(QObject::tr("Invalid thread count! (after %1)").arg(COMMAND_MAX_THREAD_COUNT));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_ROT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (!cmd.arguments().empty())
			{
				QString rotation = cmd.arguments().takeFirst().toUpper();

				// invalidate all previous rotations in case -ROT used twice
				cmd.print(QObject::tr("[ICP] Reset rotation constraints if any. Only one -%1 argument allowed").arg(COMMAND_ICP_ROT));
				transformationFilters &= (~CCCoreLib::RegistrationTools::SKIP_ROTATION);

				if (rotation == "XYZ")
				{
					cmd.print(QObject::tr("[ICP] Use all rotations"));
				}
				else if (rotation == "X")
				{
					transformationFilters |= CCCoreLib::RegistrationTools::SKIP_RYZ;
					cmd.print(QObject::tr("[ICP] Skip RYZ"));
				}
				else if (rotation == "Y")
				{
					transformationFilters |= CCCoreLib::RegistrationTools::SKIP_RXZ;
					cmd.print(QObject::tr("[ICP] Skip RXZ"));
				}
				else if (rotation == "Z")
				{
					transformationFilters |= CCCoreLib::RegistrationTools::SKIP_RXY;
					cmd.print(QObject::tr("[ICP] Skip RXY"));
				}
				else if (rotation == "NONE")
				{
					transformationFilters |= CCCoreLib::RegistrationTools::SKIP_ROTATION;
					cmd.print(QObject::tr("[ICP] Skip rotation"));
				}
				else
				{
					return cmd.error(QObject::tr("Invalid parameter: unknown rotation filter \"%1\"").arg(rotation));
				}
			}
			else
			{
				return cmd.error(QObject::tr("Missing parameter: rotation filter after \"-%1\" (XYZ/X/Y/Z/NONE)").arg(COMMAND_ICP_ROT));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_SKIP_TX))
		{
			transformationFilters |= CCCoreLib::RegistrationTools::SKIP_TX;
			cmd.print(QObject::tr("[ICP] Skip TX"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_SKIP_TY))
		{
			transformationFilters |= CCCoreLib::RegistrationTools::SKIP_TY;
			cmd.print(QObject::tr("[ICP] Skip TY"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_SKIP_TZ))
		{
			transformationFilters |= CCCoreLib::RegistrationTools::SKIP_TZ;
			cmd.print(QObject::tr("[ICP] Skip TZ"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_ICP_C2M_DIST))
		{
			useC2MDistances = true;
			cmd.print(QObject::tr("[ICP] Use C2M distances"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2M_DIST_NON_ROBUST))
		{
			robustC2MDistances = false;
			cmd.warning(QObject::tr("[ICP] Use non-robust C2M distances"));
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else if (ccCommandLineInterface::IsCommand(argument, COMMAND_C2M_NORMAL_MATCHING))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();

			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: normals matching mode after '%1'").arg(COMMAND_C2M_NORMAL_MATCHING));
			}

			QString normalsMatchingOption = cmd.arguments().takeFirst().toUpper();

			if (normalsMatchingOption == "OPPOSITE")
			{
				normalsMatching = CCCoreLib::ICPRegistrationTools::OPPOSITE_NORMALS;
				cmd.print(QObject::tr("[ICP] Use opposite normals matching mode"));
			}
			else if (normalsMatchingOption == "SAME_SIDE")
			{
				normalsMatching = CCCoreLib::ICPRegistrationTools::SAME_SIDE_NORMALS;
				cmd.print(QObject::tr("[ICP] Use same-side normals matching mode"));
			}
			else if (normalsMatchingOption == "DOUBLE_SIDED")
			{
				normalsMatching = CCCoreLib::ICPRegistrationTools::DOUBLE_SIDED_NORMALS;
				cmd.print(QObject::tr("[ICP] Use double-sided normals matching mode"));
			}
			else
			{
				return cmd.error(QObject::tr("Unknown normal matching mode: ") + normalsMatchingOption);
			}

			// local option confirmed, we can move on
			cmd.arguments().pop_front();
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	cmd.printDebug(QObject::tr("[ICP] Transfromation filter: %1").arg(transformationFilters));

	// we'll get the first two entities
	CLEntityDesc* dataAndModel[2]{nullptr, nullptr};
	{
		int index = 0;
		if (!cmd.clouds().empty())
		{
			dataAndModel[index++] = &cmd.clouds()[0];
			if (cmd.clouds().size() > 1)
			{
				dataAndModel[index++] = &cmd.clouds()[1];
			}
		}
		if (index < 2 && !cmd.meshes().empty())
		{
			dataAndModel[index++] = &cmd.meshes()[0];
			if (index < 2 && cmd.meshes().size() > 1)
			{
				dataAndModel[index++] = &cmd.meshes()[1];
			}
		}

		if (index < 2)
		{
			return cmd.error(QObject::tr("Not enough loaded entities (expect at least 2!)"));
		}
	}

	// put them in the right order (data first, model next)
	if (referenceIsFirst)
	{
		std::swap(dataAndModel[0], dataAndModel[1]);
	}

	// check that the weights (scalar fields) exist
	if (dataWeightsSFIndex >= 0 || !dataWeightsSFIndexName.isEmpty())
	{
		ccPointCloud* dataAsCloud = ccHObjectCaster::ToPointCloud(dataAndModel[0]->getEntity());
		if (dataAsCloud)
		{
			dataWeightsSFIndex = GetScalarFieldIndex(dataAsCloud, dataWeightsSFIndex, dataWeightsSFIndexName, true);
			if (dataWeightsSFIndex >= 0)
			{
				cmd.print(QObject::tr("[ICP] SF #%1 (data entity) will be used as weights").arg(dataWeightsSFIndex));
				dataAsCloud->setCurrentDisplayedScalarField(dataWeightsSFIndex);
			}
		}
	}

	if (modelWeightsSFIndex >= 0 || !modelWeightsSFIndexName.isEmpty())
	{
		ccPointCloud* modelAsCloud = ccHObjectCaster::ToPointCloud(dataAndModel[1]->getEntity());
		if (modelAsCloud)
		{
			modelWeightsSFIndex = GetScalarFieldIndex(modelAsCloud, modelWeightsSFIndex, modelWeightsSFIndexName, true);
			if (modelWeightsSFIndex >= 0)
			{
				cmd.print(QObject::tr("[ICP] SF #%1 (model entity) will be used as weights").arg(modelWeightsSFIndex));
				modelAsCloud->setCurrentDisplayedScalarField(modelWeightsSFIndex);
			}
		}
	}

	ccGLMatrix transMat;
	double     finalError      = 0.0;
	double     finalScale      = 1.0;
	unsigned   finalPointCount = 0;

	CCCoreLib::ICPRegistrationTools::Parameters parameters;
	{
		parameters.convType                 = (iterationCount != 0 ? CCCoreLib::ICPRegistrationTools::MAX_ITER_CONVERGENCE : CCCoreLib::ICPRegistrationTools::MAX_ERROR_CONVERGENCE);
		parameters.minRMSDecrease           = minErrorDiff;
		parameters.nbMaxIterations          = iterationCount;
		parameters.adjustScale              = adjustScale;
		parameters.filterOutFarthestPoints  = enableFarthestPointRemoval;
		parameters.samplingLimit            = randomSamplingLimit;
		parameters.finalOverlapRatio        = overlap / 100.0;
		parameters.transformationFilters    = transformationFilters;
		parameters.maxThreadCount           = maxThreadCount;
		parameters.useC2MSignedDistances    = useC2MDistances;
		parameters.robustC2MSignedDistances = robustC2MDistances;
		parameters.normalsMatching          = normalsMatching;
	}

	if (ccRegistrationTools::ICP(dataAndModel[0]->getEntity(),
	                             dataAndModel[1]->getEntity(),
	                             transMat,
	                             finalScale,
	                             finalError,
	                             finalPointCount,
	                             parameters,
	                             dataWeightsSFIndex >= 0,
	                             modelWeightsSFIndex >= 0,
	                             cmd.widgetParent()))
	{
		ccHObject* data = dataAndModel[0]->getEntity();
		data->applyGLTransformation_recursive(&transMat);
		cmd.print(QObject::tr("Entity '%1' has been registered").arg(data->getName()));
		cmd.print(QObject::tr("RMS: %1").arg(finalError));
		cmd.print(QObject::tr("Number of points used for final step: %1").arg(finalPointCount));

		// save matrix in a separate text file
		{
			QString txtFilename = QObject::tr("%1/%2_REGISTRATION_MATRIX").arg(dataAndModel[0]->path, dataAndModel[0]->basename);
			if (cmd.addTimestamp())
			{
				QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_hh'h'mm_ss_zzz");
				txtFilename += QString("_%1").arg(timestamp);
			}
			txtFilename += ".txt";
			QFile txtFile(txtFilename);
			if (txtFile.open(QIODevice::WriteOnly | QIODevice::Text))
			{
				QTextStream txtStream(&txtFile);
				txtStream << transMat.toString(cmd.numericalPrecision(), ' ') << endl;
				txtFile.close();
			}
			else
			{
				cmd.warning("Failed to save the registration matrix to file " + txtFilename);
			}
		}

		dataAndModel[0]->basename += QObject::tr("_REGISTERED");
		if (cmd.autoSaveMode())
		{
			QString errorStr = cmd.exportEntity(*dataAndModel[0]);
			if (!errorStr.isEmpty())
			{
				return cmd.error(errorStr);
			}
		}
	}
	else
	{
		return false;
	}

	return true;
}

CommandChangePLYExportFormat::CommandChangePLYExportFormat()
    : ccCommandLineInterface::Command(QObject::tr("Change PLY output format"), COMMAND_PLY_EXPORT_FORMAT)
{
}

bool CommandChangePLYExportFormat::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: format (ASCII, BINARY_LE, or BINARY_BE) after '%1'").arg(COMMAND_PLY_EXPORT_FORMAT));
	}

	// if (fileFilter != PlyFilter::GetFileFilter())
	//	cmd.warning(QObject::tr("Argument '%1' is only applicable to PLY format!").arg(argument));

	QString plyFormat = cmd.arguments().takeFirst().toUpper();
	// printf("%s\n",qPrintable(plyFormat));

	if (plyFormat == "ASCII")
	{
		PlyFilter::SetDefaultOutputFormat(PLY_ASCII);
	}
	else if (plyFormat == "BINARY_BE")
	{
		PlyFilter::SetDefaultOutputFormat(PLY_BIG_ENDIAN);
	}
	else if (plyFormat == "BINARY_LE")
	{
		PlyFilter::SetDefaultOutputFormat(PLY_LITTLE_ENDIAN);
	}
	else
	{
		return cmd.error(QObject::tr("Invalid PLY format! ('%1')").arg(plyFormat));
	}

	return true;
}

CommandForceNormalsComputation::CommandForceNormalsComputation()
    : ccCommandLineInterface::Command(QObject::tr("Compute structured cloud normals"), COMMAND_COMPUTE_GRIDDED_NORMALS)
{
}

bool CommandForceNormalsComputation::process(ccCommandLineInterface& cmd)
{
	// simply change the default filter behavior
	cmd.fileLoadingParams().autoComputeNormals = true;

	return true;
}

CommandSave::CommandSave(const QString& name, const QString& keyword)
    : ccCommandLineInterface::Command(name, keyword)
{
}

bool CommandSave::ParseFileNames(ccCommandLineInterface& cmd, QStringList& fileNames)
{
	//
	// File list is space separated, but can use quotes to include spaces in the file names
	//
	auto argument = cmd.arguments().takeFirst();
	while (!argument.isEmpty())
	{
		auto firstChar = argument.at(0);
		if (firstChar == '\'' || firstChar == '\"')
		{
			auto end = argument.indexOf(firstChar, 1);
			if (end == -1)
			{
				return cmd.error(QObject::tr("A file starting with %1 does not have a closing %1").arg(firstChar));
			}

			fileNames.push_back(argument.mid(1, end - 1));
			argument.remove(0, end + 1);
			if (argument.startsWith(' '))
			{
				argument.remove(0, 1);
			}
		}
		else
		{
			auto end = argument.indexOf(' ');
			if (end == -1)
			{
				end = argument.length();
			}
			fileNames.push_back(argument.left(end));
			argument.remove(0, end + 1);
		}
	}
	return true;
}

void CommandSave::SetFileDesc(CLEntityDesc& desc, const QString& fileName)
{
	QFileInfo fInfo(fileName);
	desc.basename = fInfo.fileName();
	desc.path     = fInfo.filePath().left(fInfo.filePath().length() - fInfo.fileName().length());
}

CommandSaveClouds::CommandSaveClouds()
    : CommandSave(QObject::tr("Save clouds"), COMMAND_SAVE_CLOUDS)
{
}

bool CommandSaveClouds::process(ccCommandLineInterface& cmd)
{
	bool        allAtOnce    = false;
	bool        setFileNames = false;
	QStringList fileNames;

	// look for additional parameters
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (argument.toUpper() == OPTION_ALL_AT_ONCE)
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			allAtOnce = true;
		}
		else if (argument.startsWith(OPTION_FILE_NAMES))
		{
			cmd.arguments().pop_front();
			setFileNames = true;
			if (!ParseFileNames(cmd, fileNames))
			{
				return false;
			}
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	if (setFileNames && allAtOnce && fileNames.size() != 1)
	{
		return cmd.error(QObject::tr("Invalid parameter: specified %1 file names, but ALL_AT_ONCE is on").arg(fileNames.size()));
	}
	if (setFileNames && !allAtOnce && fileNames.size() != cmd.clouds().size())
	{
		return cmd.error(QObject::tr("Invalid parameter: specified %1 file names, but there are %2 clouds").arg(fileNames.size()).arg(cmd.clouds().size()));
	}

	QString ext              = cmd.cloudExportExt();
	bool    autoAddTimestamp = cmd.addTimestamp();
	if (setFileNames)
	{
		cmd.toggleAddTimestamp(false);
		cmd.setCloudExportFormat(cmd.cloudExportFormat(), QString());

		if (!allAtOnce)
		{
			for (int i = 0; i < fileNames.size(); ++i)
			{
				SetFileDesc(cmd.clouds()[i], fileNames[i]);
			}
		}
	}

	bool res = cmd.saveClouds(QString(), allAtOnce, allAtOnce && setFileNames ? &fileNames[0] : nullptr);

	if (setFileNames)
	{
		cmd.toggleAddTimestamp(autoAddTimestamp);
		cmd.setCloudExportFormat(cmd.cloudExportFormat(), ext);
	}

	return res;
}

CommandSaveMeshes::CommandSaveMeshes()
    : CommandSave(QObject::tr("Save meshes"), COMMAND_SAVE_MESHES)
{
}

bool CommandSaveMeshes::process(ccCommandLineInterface& cmd)
{
	bool        allAtOnce    = false;
	bool        setFileNames = false;
	QStringList fileNames;

	// look for additional parameters
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (argument.toUpper() == OPTION_ALL_AT_ONCE)
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			allAtOnce = true;
		}
		else if (argument.left(sizeof(OPTION_FILE_NAMES) - 1).toUpper() == OPTION_FILE_NAMES)
		{
			cmd.arguments().pop_front();
			setFileNames = true;
			if (!ParseFileNames(cmd, fileNames))
			{
				return false;
			}
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	if (setFileNames && allAtOnce && fileNames.size() != 1)
	{
		return cmd.error(QObject::tr("Invalid parameter: specified %1 file names, but ALL_AT_ONCE is on").arg(fileNames.size()));
	}
	if (setFileNames && !allAtOnce && fileNames.size() != cmd.meshes().size())
	{
		return cmd.error(QObject::tr("Invalid parameter: specified %1 file names, but there are %2 meshes").arg(fileNames.size()).arg(cmd.meshes().size()));
	}

	QString ext              = cmd.meshExportExt();
	bool    autoAddTimestamp = cmd.addTimestamp();
	if (setFileNames)
	{
		cmd.toggleAddTimestamp(false);
		cmd.setMeshExportFormat(cmd.meshExportFormat(), QString());

		if (!allAtOnce)
		{
			for (int i = 0; i < fileNames.size(); ++i)
			{
				SetFileDesc(cmd.meshes()[i], fileNames[i]);
			}
		}
	}

	bool res = cmd.saveMeshes(QString(), allAtOnce, allAtOnce && setFileNames ? &fileNames[0] : nullptr);

	if (setFileNames)
	{
		cmd.toggleAddTimestamp(autoAddTimestamp);
		cmd.setMeshExportFormat(cmd.meshExportFormat(), ext);
	}

	return res;
}

CommandAutoSave::CommandAutoSave()
    : ccCommandLineInterface::Command(QObject::tr("Auto save state"), COMMAND_AUTO_SAVE)
{
}

bool CommandAutoSave::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: option after '%1' (%2/%3)").arg(COMMAND_AUTO_SAVE, OPTION_ON, OPTION_OFF));
	}

	QString option = cmd.arguments().takeFirst().toUpper();
	if (option == OPTION_ON)
	{
		cmd.print(QObject::tr("Auto-save is enabled"));
		cmd.toggleAutoSaveMode(true);
	}
	else if (option == OPTION_OFF)
	{
		cmd.print(QObject::tr("Auto-save is disabled"));
		cmd.toggleAutoSaveMode(false);
	}
	else
	{
		return cmd.error(QObject::tr("Unrecognized option after '%1' (%2 or %3 expected)").arg(COMMAND_AUTO_SAVE, OPTION_ON, OPTION_OFF));
	}

	return true;
}

CommandLogFile::CommandLogFile()
    : ccCommandLineInterface::Command(QObject::tr("Set log file"), COMMAND_LOG_FILE)
{
}

bool CommandLogFile::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: filename after '%1'").arg(COMMAND_LOG_FILE));
	}

	QString filename = cmd.arguments().takeFirst();
	if (!ccConsole::TheInstance(false))
	{
		assert(cmd.silentMode());
		ccConsole::Init();
	}

	return ccConsole::TheInstance()->setLogFile(filename);
}

CommandSelectEntities::CommandSelectEntities()
    : ccCommandLineInterface::Command(QObject::tr("Select entities"), COMMAND_SELECT_ENTITIES)
{
}

bool CommandSelectEntities::process(ccCommandLineInterface& cmd)
{
	// option handling
	// look for additional parameters
	ccCommandLineInterface::SelectEntitiesOptions options;
	bool                                          selectMeshes = false;
	bool                                          selectClouds = false;
	while (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front().toUpper();
		if (ccCommandLineInterface::IsCommand(argument, OPTION_ALL))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			options.selectAll = true;
			// no other params needed
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_FIRST))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			// it requires a number after the argument
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: number of entities after %1").arg(OPTION_FIRST));
			}
			bool ok;
			options.firstNr = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid number after -%1").arg(OPTION_FIRST));
			}
			options.selectFirst = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_LAST))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			// it requires a number after the argument
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: number of entities after %1").arg(OPTION_LAST));
			}
			bool ok;
			options.lastNr = cmd.arguments().takeFirst().toUInt(&ok);
			if (!ok)
			{
				return cmd.error(QObject::tr("Invalid number after -%1").arg(OPTION_LAST));
			}
			options.selectLast = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_REGEX))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			options.selectRegex = true;
			// it requires a string after the argument
			if (cmd.arguments().empty())
			{
				return cmd.error(QObject::tr("Missing parameter: regex string after %1").arg(OPTION_REGEX));
			}
			QString regexString = cmd.arguments().takeFirst();
			options.regex.setPattern(regexString);
			if (!options.regex.isValid())
			{
				return cmd.error(QObject::tr("Invalid regex pattern: %1").arg(options.regex.errorString()));
			}
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_NOT))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			options.reverse = true;
			// no other params needed
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_CLOUD))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			selectClouds = true;
		}
		else if (ccCommandLineInterface::IsCommand(argument, OPTION_MESH))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			selectMeshes = true;
		}
		else
		{
			break; // as soon as we encounter an unrecognized argument, we break the local loop to go back to the main one!
		}
	}

	if (options.selectAll)
	{
		// overwrite any other mode (first/last/regex)
		options.selectFirst = false;
		options.selectLast  = false;
		options.selectRegex = false;
		if (options.reverse)
		{
			// NONE
			cmd.print("No entities will be selected.");
		}
		else
		{
			// ALL
			cmd.print("All entities will be selected. Other options will be ignored except -NOT.");
		}
	}

	if (options.selectFirst)
	{
		if (options.reverse)
		{
			if (options.selectLast)
			{
				// not first {nr} and not last {nr}
				cmd.print(QObject::tr("First %1 and last %2 entity(ies) will not be selected").arg(options.firstNr).arg(options.lastNr));
			}
			else
			{
				// not first {nr}
				cmd.print(QObject::tr("First %1 entity(ies) will not be selected").arg(options.firstNr));
			}
		}
		else
		{
			// first {nr}
			cmd.print(QObject::tr("First %1 entity(ies) will be selected").arg(options.firstNr));
		}
	}

	if (options.selectLast)
	{
		if (options.reverse)
		{
			if (!options.selectFirst)
			{
				// not last {nr}
				cmd.print(QObject::tr("Last %1 entity(ies) will not be selected").arg(options.lastNr));
			}
		}
		else
		{
			// last {nr}
			cmd.print(QObject::tr("Last %1 entity(ies) will be selected").arg(options.lastNr));
		}
	}

	if (options.selectRegex)
	{
		if (options.reverse)
		{
			// regex not matches
			cmd.print(QObject::tr("Entities with name matches the regex /%1/ will not be selected.").arg(options.regex.pattern()));
		}
		else
		{
			// regex matches
			cmd.print(QObject::tr("Entities with name matches the regex /%1/ will be selected.").arg(options.regex.pattern()));
		}
	}

	// no option was set
	if (!options.selectFirst && !options.selectLast && !options.selectRegex && !options.selectAll)
	{
		return cmd.error(QObject::tr("Missing parameter(s): any of the option (%1,%2,%3,%4) expected after %5")
		                     .arg(OPTION_ALL)
		                     .arg(OPTION_FIRST)
		                     .arg(OPTION_LAST)
		                     .arg(OPTION_REGEX)
		                     .arg(COMMAND_SELECT_ENTITIES));
	}

	// no entity type was selected so select both clouds and meshes
	if (!selectClouds && !selectMeshes)
	{
		selectClouds = true;
		selectMeshes = true;
	}

	if (selectClouds)
	{
		cmd.print(QObject::tr("[Select clouds]"));
		if (!cmd.selectClouds(options))
		{
			// error message already sent
			return false;
		}
	}

	if (selectMeshes)
	{
		cmd.print(QObject::tr("[Select meshes]"));
		if (!cmd.selectMeshes(options))
		{
			// error message already sent
			return false;
		}
	}

	return true;
}

CommandClear::CommandClear()
    : ccCommandLineInterface::Command(QObject::tr("Clear"), COMMAND_CLEAR)
{
}

bool CommandClear::process(ccCommandLineInterface& cmd)
{
	cmd.removeClouds(false);
	cmd.removeMeshes(false);
	return true;
}

CommandClearClouds::CommandClearClouds()
    : ccCommandLineInterface::Command(QObject::tr("Clear clouds"), COMMAND_CLEAR_CLOUDS)
{
}

bool CommandClearClouds::process(ccCommandLineInterface& cmd)
{
	cmd.removeClouds(false);
	return true;
}

CommandPopClouds::CommandPopClouds()
    : ccCommandLineInterface::Command(QObject::tr("Pop cloud"), COMMAND_POP_CLOUDS)
{
}

bool CommandPopClouds::process(ccCommandLineInterface& cmd)
{
	cmd.removeClouds(true);
	return true;
}

CommandClearMeshes::CommandClearMeshes()
    : ccCommandLineInterface::Command(QObject::tr("Clear meshes"), COMMAND_CLEAR_MESHES)
{
}

bool CommandClearMeshes::process(ccCommandLineInterface& cmd)
{
	cmd.removeMeshes(false);
	return true;
}

CommandPopMeshes::CommandPopMeshes()
    : ccCommandLineInterface::Command(QObject::tr("Pop mesh"), COMMAND_POP_MESHES)
{
}

bool CommandPopMeshes::process(ccCommandLineInterface& cmd)
{
	cmd.removeMeshes(true);
	return true;
}

CommandSetNoTimestamp::CommandSetNoTimestamp()
    : ccCommandLineInterface::Command(QObject::tr("No timestamp"), COMMAND_NO_TIMESTAMP)
{
}

bool CommandSetNoTimestamp::process(ccCommandLineInterface& cmd)
{
	cmd.toggleAddTimestamp(false);
	return true;
}

CommandMoment::CommandMoment()
    : ccCommandLineInterface::Command(QObject::tr("1st order moment"), COMMAND_MOMENT)
{
}

bool CommandMoment::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: kernel size after %1").arg(COMMAND_MOMENT));
	}

	bool                paramOk    = false;
	QString             kernelStr  = cmd.arguments().takeFirst();
	PointCoordinateType kernelSize = static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
	if (!paramOk)
	{
		return cmd.error(QObject::tr("Failed to read a numerical parameter: kernel size. Got '%1' instead.").arg(kernelStr));
	}
	cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute first order moment! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_MOMENT));
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	if (ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::MomentOrder1, 0, kernelSize, entities, nullptr, cmd.widgetParent()))
	{
		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds(QObject::tr("MOMENT_KERNEL_%2").arg(kernelSize)))
		{
			return false;
		}
	}
	return true;
}

CommandFeature::CommandFeature()
    : ccCommandLineInterface::Command(QObject::tr("Feature"), COMMAND_FEATURE)
{
}

bool CommandFeature::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: feature type after \"-%1\"").arg(COMMAND_FEATURE));
	}

	QString                               featureTypeStr = cmd.arguments().takeFirst().toUpper();
	CCCoreLib::Neighbourhood::GeomFeature featureType;

	if (featureTypeStr == "SUM_OF_EIGENVALUES")
	{
		featureType = CCCoreLib::Neighbourhood::EigenValuesSum;
	}
	else if (featureTypeStr == "OMNIVARIANCE")
	{
		featureType = CCCoreLib::Neighbourhood::Omnivariance;
	}
	else if (featureTypeStr == "EIGENTROPY")
	{
		featureType = CCCoreLib::Neighbourhood::EigenEntropy;
	}
	else if (featureTypeStr == "ANISOTROPY")
	{
		featureType = CCCoreLib::Neighbourhood::Anisotropy;
	}
	else if (featureTypeStr == "PLANARITY")
	{
		featureType = CCCoreLib::Neighbourhood::Planarity;
	}
	else if (featureTypeStr == "LINEARITY")
	{
		featureType = CCCoreLib::Neighbourhood::Linearity;
	}
	else if (featureTypeStr == "PCA1")
	{
		featureType = CCCoreLib::Neighbourhood::PCA1;
	}
	else if (featureTypeStr == "PCA2")
	{
		featureType = CCCoreLib::Neighbourhood::PCA2;
	}
	else if (featureTypeStr == "SURFACE_VARIATION")
	{
		featureType = CCCoreLib::Neighbourhood::SurfaceVariation;
	}
	else if (featureTypeStr == "SPHERICITY")
	{
		featureType = CCCoreLib::Neighbourhood::Sphericity;
	}
	else if (featureTypeStr == "VERTICALITY")
	{
		featureType = CCCoreLib::Neighbourhood::Verticality;
	}
	else if (featureTypeStr == "EIGENVALUE1")
	{
		featureType = CCCoreLib::Neighbourhood::EigenValue1;
	}
	else if (featureTypeStr == "EIGENVALUE2")
	{
		featureType = CCCoreLib::Neighbourhood::EigenValue2;
	}
	else if (featureTypeStr == "EIGENVALUE3")
	{
		featureType = CCCoreLib::Neighbourhood::EigenValue3;
	}
	else
	{
		return cmd.error(QObject::tr("Invalid feature type after \"-%1\". Got '%2' instead of:\n\
- SUM_OF_EIGENVALUES\n\
- OMNIVARIANCE\n\
- EIGENTROPY\n\
- ANISOTROPY\n\
- PLANARITY\n\
- LINEARITY\n\
- PCA1\n\
- PCA2\n\
- SURFACE_VARIATION\n\
- SPHERICITY\n\
- VERTICALITY\n\
- EIGENVALUE1\n\
- EIGENVALUE2\n\
- EIGENVALUE3")
		                     .arg(COMMAND_FEATURE, featureTypeStr));
	}

	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: kernel size after feature type"));
	}

	bool                paramOk    = false;
	QString             kernelStr  = cmd.arguments().takeFirst();
	PointCoordinateType kernelSize = static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
	if (!paramOk)
	{
		return cmd.error(QObject::tr("Failed to read a numerical parameter: kernel size. Got '%1' instead.").arg(kernelStr));
	}
	cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));

	if (cmd.clouds().empty())
	{
		return cmd.error(QObject::tr("No point cloud on which to compute feature! (be sure to open one with \"-%1 [cloud filename]\" before \"-%2\")").arg(COMMAND_OPEN, COMMAND_FEATURE));
	}

	// Call MainWindow generic method on all available clouds
	ccHObject::Container entities;
	{
		entities.resize(cmd.clouds().size());
		for (size_t i = 0; i < cmd.clouds().size(); ++i)
		{
			entities[i] = cmd.clouds()[i].pc;
		}
	}

	if (!ccLibAlgorithms::ComputeGeomCharacteristic(CCCoreLib::GeometricalAnalysisTools::Feature, featureType, kernelSize, entities, nullptr, cmd.widgetParent()))
	{
		return cmd.error(QObject::tr("The computation of some geometric features failed."));
	}

	// on success, update the cloud names
	QString fileNameExt = QObject::tr("%1_FEATURE_KERNEL_%2").arg(featureTypeStr).arg(kernelSize);
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		CLCloudDesc& desc = cmd.clouds()[i];
		desc.basename += "_" + fileNameExt;
		desc.pc->setName(entities[i]->getName() + QObject::tr(".%1_feature(%2)").arg(featureTypeStr.toLower()).arg(kernelSize));
	}

	// save output
	if (cmd.autoSaveMode() && !cmd.saveClouds())
	{
		return false;
	}

	return true;
}

CommandDebugCmdLine::CommandDebugCmdLine()
    : ccCommandLineInterface::Command(QObject::tr("Debug Command Line"), COMMAND_DEBUG)
{
}

bool CommandDebugCmdLine::process(ccCommandLineInterface& cmd)
{
	cmd.print("******************************************");
	cmd.print("Number of selected clouds: " + QString::number(cmd.clouds().size()));
	cmd.print("Number of selected meshes: " + QString::number(cmd.meshes().size()));

	cmd.print("******************************************");
	const QStringList& arguments = cmd.arguments();
	cmd.print("Number of arguments: " + QString::number(arguments.size()));
	for (int i = 0; i < arguments.size(); ++i)
	{
		cmd.print(QString("Argument #%1: < %2 >").arg(i + 1).arg(arguments[i]));
	}

	cmd.print("******************************************");
	cmd.print("[Loading parameters]");
	cmd.print(QObject::tr("Global shift set: ") + (cmd.fileLoadingParams().coordinatesShiftEnabled ? "yes" : "no"));
	cmd.print(QObject::tr("Global shift: (%1, %2, %3)").arg(cmd.fileLoadingParams().coordinatesShift.x).arg(cmd.fileLoadingParams().coordinatesShift.y).arg(cmd.fileLoadingParams().coordinatesShift.z));

	cmd.print("******************************************");
	cmd.print("[Export parameters]");
	cmd.print("Cloud export format: " + cmd.cloudExportFormat());
	cmd.print("Mesh export format: " + cmd.meshExportFormat());
	cmd.print("Group export format: " + cmd.hierarchyExportFormat());

	cmd.print("******************************************");
	cmd.print("[Other parameters]");
	cmd.print(QObject::tr("Silent mode: ") + (cmd.silentMode() ? "ON" : "OFF"));
	cmd.print(QObject::tr("Auto save: ") + (cmd.autoSaveMode() ? "ON" : "OFF"));
	cmd.print(QObject::tr("Auto add timestamp: ") + (cmd.addTimestamp() ? "ON" : "OFF"));
	cmd.print(QObject::tr("Numerical precision: %1").arg(cmd.numericalPrecision()));

	return true;
}

CommandSetVerbosity::CommandSetVerbosity()
    : ccCommandLineInterface::Command(QObject::tr("Set Verbosity"), COMMAND_VERBOSITY)
{
}

bool CommandSetVerbosity::process(ccCommandLineInterface& cmd)
{
	if (cmd.arguments().empty())
	{
		return cmd.error(QObject::tr("Missing parameter: verbosity level after: %1").arg(COMMAND_VERBOSITY));
	}

	bool ok             = false;
	int  verbosityLevel = cmd.arguments().takeFirst().toInt(&ok);
	if (!ok)
	{
		return cmd.error(QObject::tr("Invalid verbosity level %1").arg(verbosityLevel));
	}
	else
	{
		cmd.print(QObject::tr("Set verbosity level to %1").arg(verbosityLevel));
		ccLog::SetVerbosityLevel(verbosityLevel);
	}

	return true;
}

CommandComputeDistancesFromSensor::CommandComputeDistancesFromSensor()
    : ccCommandLineInterface::Command(QObject::tr("Compute distances from sensor"), COMMAND_COMPUTE_DISTANCES_FROM_SENSOR)
{
}

bool CommandComputeDistancesFromSensor::process(ccCommandLineInterface& cmd)
{
	bool squared = false;

	// optional parameter: compute squared distances
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, OPTION_SQUARED))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			squared = true;
			cmd.print(QObject::tr("Squared distances"));
		}
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	for (ccHObject* entity : entities)
	{
		unsigned  childrenNumber = entity->getChildrenNumber();
		ccSensor* sensor{nullptr};
		for (unsigned childPos = 0; childPos < childrenNumber; childPos++)
		{
			sensor = ccHObjectCaster::ToSensor(entity->getChild(childPos));
			if (sensor)
				break; // once a sensor is found, break the loop
		}

		if (!sensor)
			continue;

		assert(sensor); // at this step we should have a sensor associated to the cloud

		// get associated cloud
		ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity);
		if (!cloud)
		{
			cmd.error(QObject::tr("Do not manage to associate the sensor with a cloud"));
			return false;
		}

		// sensor center
		CCVector3 sensorCenter;
		if (!sensor->getActiveAbsoluteCenter(sensorCenter))
		{
			cmd.error(QObject::tr("Sensor center not detected"));
			return false;
		}

		// set up a new scalar field
		const char* defaultRangesSFname = squared ? CC_DEFAULT_SQUARED_RANGES_SF_NAME : CC_DEFAULT_RANGES_SF_NAME;
		int         sfIdx               = cloud->getScalarFieldIndexByName(defaultRangesSFname);
		if (sfIdx < 0)
		{
			sfIdx = cloud->addScalarField(defaultRangesSFname);
			if (sfIdx < 0)
			{
				cmd.error(QObject::tr("Not enough memory!"));
				return false;
			}
		}
		CCCoreLib::ScalarField* distances = cloud->getScalarField(sfIdx);

		// perform computation
		for (unsigned i = 0; i < cloud->size(); ++i)
		{
			const CCVector3* P = cloud->getPoint(i);
			ScalarType       s = static_cast<ScalarType>(squared ? (*P - sensorCenter).norm2() : (*P - sensorCenter).norm());
			distances->setValue(i, s);
		}

		distances->computeMinAndMax();

		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds("RANGES"))
		{
			return false;
		}
	}

	return true;
}

CommandComputeScatteringAngles::CommandComputeScatteringAngles()
    : ccCommandLineInterface::Command(QObject::tr("Compute scattering angles"), COMMAND_COMPUTE_SCATTERING_ANGLES)
{
}

bool CommandComputeScatteringAngles::process(ccCommandLineInterface& cmd)
{
	bool toDegreeFlag = false;

	// optional parameter: compute squared distances
	if (!cmd.arguments().empty())
	{
		QString argument = cmd.arguments().front();
		if (ccCommandLineInterface::IsCommand(argument, OPTION_DEGREES))
		{
			// local option confirmed, we can move on
			cmd.arguments().pop_front();
			toDegreeFlag = true;
			cmd.print(QObject::tr("Scattering angles in degrees"));
		}
	}

	// Call MainWindow generic method
	ccHObject::Container entities;
	entities.resize(cmd.clouds().size());
	for (size_t i = 0; i < cmd.clouds().size(); ++i)
	{
		entities[i] = cmd.clouds()[i].pc;
	}

	for (ccHObject* entity : entities)
	{
		unsigned  childrenNumber = entity->getChildrenNumber();
		ccSensor* sensor{nullptr};
		for (unsigned childPos = 0; childPos < childrenNumber; childPos++)
		{
			sensor = ccHObjectCaster::ToSensor(entity->getChild(childPos));
			if (sensor)
				break; // once a sensor is found, break the loop
		}

		if (!sensor)
			continue;

		assert(sensor); // at this step we should have a sensor associated to the cloud

		// get associated cloud
		ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(entity);
		if (!cloud)
		{
			cmd.error(QObject::tr("Do not manage to associate the sensor with a cloud"));
			return false;
		}

		if (!cloud->hasNormals())
		{
			cmd.print(QObject::tr(("The cloud must have normals for scattering angles calculations, skip calculation for cloud " + cloud->getName()).toLatin1()));
			continue;
		}

		// sensor center
		CCVector3 sensorCenter;
		if (!sensor->getActiveAbsoluteCenter(sensorCenter))
		{
			cmd.error(QObject::tr("Sensor center not detected"));
			return false;
		}

		// set up a new scalar field
		const char* defaultScatAnglesSFname = toDegreeFlag ? CC_DEFAULT_DEG_SCATTERING_ANGLES_SF_NAME : CC_DEFAULT_RAD_SCATTERING_ANGLES_SF_NAME;
		int         sfIdx                   = cloud->getScalarFieldIndexByName(defaultScatAnglesSFname);
		if (sfIdx < 0)
		{
			sfIdx = cloud->addScalarField(defaultScatAnglesSFname);
			if (sfIdx < 0)
			{
				cmd.error(QObject::tr("Not enough memory!"));
				return false;
			}
		}
		CCCoreLib::ScalarField* angles = cloud->getScalarField(sfIdx);

		// perform computation
		for (unsigned i = 0; i < cloud->size(); ++i)
		{
			// the point position
			const CCVector3* P = cloud->getPoint(i);

			// build the ray
			CCVector3 ray = *P - sensorCenter;
			ray.normalize();

			// get the current normal
			CCVector3 normal(cloud->getPointNormal(i));
			// normal.normalize(); //should already be the case!

			// compute the angle
			PointCoordinateType cosTheta = ray.dot(normal);
			ScalarType          theta    = std::acos(std::min(std::abs(cosTheta), 1.0f));

			if (toDegreeFlag)
			{
				theta = CCCoreLib::RadiansToDegrees(theta);
			}

			angles->setValue(i, theta);
		}

		// save output
		if (cmd.autoSaveMode() && !cmd.saveClouds("SCATTERING_ANGLES"))
		{
			return false;
		}
	}

	return true;
}
